commit f0f83e7ed7987bb10736189e3edf9858884da3d3 Author: chanbook <648715275@qq.com> Date: Wed Jul 27 21:01:00 2022 +0800 init diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..735bd70 --- /dev/null +++ b/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.7 + + + com.zhangshu + chat-demo + 0.0.1-SNAPSHOT + chat-demo + chat-demo + + 1.8 + 5.7.22 + 1.4.2.Final + 3.0.0 + 1.5.21 + 3.0.0 + 2.0.7 + 1.5.17 + 3.5.1 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + cn.hutool + hutool-all + ${hutool.version} + + + io.springfox + springfox-boot-starter + ${swagger2.starter.version} + + + io.swagger + swagger-models + + + + + io.swagger + swagger-models + ${swagger2.model.version} + + + com.github.xiaoymin + knife4j-spring-boot-starter + 3.0.3 + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security + + + com.dtflys.forest + forest-spring-boot-starter + ${forest.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis.plus.version} + + + com.h2database + h2 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/zhangshu/chat/demo/ChatDemoApplication.java b/src/main/java/com/zhangshu/chat/demo/ChatDemoApplication.java new file mode 100644 index 0000000..bf0a438 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/ChatDemoApplication.java @@ -0,0 +1,13 @@ +package com.zhangshu.chat.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ChatDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(ChatDemoApplication.class, args); + } + +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/AgoraProperties.java b/src/main/java/com/zhangshu/chat/demo/config/AgoraProperties.java new file mode 100644 index 0000000..89bd850 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/AgoraProperties.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * JwtProperties + * + * @author chanbook + * @date 2022/3/10 17:59 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "agora") +public class AgoraProperties { + private String appId; + private String appCertificate; + private Integer tokenExpiration; + private Integer privilegeExpiration; +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/BusinessException.java b/src/main/java/com/zhangshu/chat/demo/config/BusinessException.java new file mode 100644 index 0000000..3eb3c32 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/BusinessException.java @@ -0,0 +1,13 @@ +package com.zhangshu.chat.demo.config; + +/** + * BusinessException + * + * + * @date 2022/3/8 10:55 + */ +public class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/CorsFilter.java b/src/main/java/com/zhangshu/chat/demo/config/CorsFilter.java new file mode 100644 index 0000000..3ee79ef --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/CorsFilter.java @@ -0,0 +1,29 @@ +package com.zhangshu.chat.demo.config; + +import org.springframework.context.annotation.Configuration; + +import javax.servlet.*; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * CorsFilter + * + * + * @date 2022/1/29 16:12 + */ +@WebFilter(filterName = "CorsFilter ") +@Configuration +public class CorsFilter implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) res; + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", "*"); + chain.doFilter(req, res); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/GlobalExceptionHandler.java b/src/main/java/com/zhangshu/chat/demo/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..e5d781d --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/GlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package com.zhangshu.chat.demo.config; + +import com.zhangshu.chat.demo.dto.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.List; + +@Slf4j +@ResponseBody +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + log.error(ex.getMessage(), ex); + List allErrors = ex.getBindingResult().getAllErrors(); + if (!CollectionUtils.isEmpty(allErrors)) { + for (ObjectError error : allErrors) { + return ResponseEntity.status(HttpStatus.OK).body(CommonResult.fail(HttpStatus.BAD_REQUEST.value(), error.getDefaultMessage())); + } + } + return ResponseEntity.status(HttpStatus.OK).body(CommonResult.fail(HttpStatus.BAD_REQUEST.value(), ex.getMessage())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + log.error(ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.OK).body(CommonResult.fail(HttpStatus.BAD_REQUEST.value(), ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity all(Exception ex) { + log.error(ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.OK).body(CommonResult.fail(ex.getMessage())); + } + + @ExceptionHandler(BusinessException.class) + public ResponseEntity business(BusinessException ex) { + log.error(ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.OK).body(CommonResult.fail(HttpStatus.BAD_REQUEST.value(), ex.getMessage())); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/JwtProperties.java b/src/main/java/com/zhangshu/chat/demo/config/JwtProperties.java new file mode 100644 index 0000000..369b59d --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/JwtProperties.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * JwtProperties + * + * @author chanbook + * @date 2022/3/10 17:59 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + private String header; + private String prefix; + private String secret; + private Long expiration; +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/JwtProvider.java b/src/main/java/com/zhangshu/chat/demo/config/JwtProvider.java new file mode 100644 index 0000000..3605ab6 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/JwtProvider.java @@ -0,0 +1,58 @@ +package com.zhangshu.chat.demo.config; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * JwtUtil + * + * @date 2022/3/20 16:51 + */ +@Component +public class JwtProvider { + @Autowired + JwtProperties jwtProperties; + @Autowired + UserDetailsService userDetailsService; + + public String createToken(UserInfo info) { + Map payloadMap = BeanUtil.beanToMap(info); + return JWTUtil.createToken(payloadMap, jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + public boolean checkToken(String token) { + return JWTUtil.verify(token, jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + public UserInfo parseToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + return payloads.toBean(UserInfo.class); + } + + public Authentication getAuthentication(String token) { + UserInfo userInfo = parseToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername()); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(jwtProperties.getHeader()); + if (bearerToken != null && bearerToken.startsWith(jwtProperties.getPrefix())) { + return bearerToken.substring(jwtProperties.getPrefix().length() + 1); + } + return ""; + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/JwtTokenAuthenticationFilter.java b/src/main/java/com/zhangshu/chat/demo/config/JwtTokenAuthenticationFilter.java new file mode 100644 index 0000000..9868b34 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/JwtTokenAuthenticationFilter.java @@ -0,0 +1,45 @@ +package com.zhangshu.chat.demo.config; + +import cn.hutool.core.util.StrUtil; +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.util.ResponseWriteUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +public class JwtTokenAuthenticationFilter extends GenericFilterBean { + @Autowired + JwtProvider jwtProvider; + @Autowired + WhiteListHandler whiteListHandler; + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + try { + Authentication auth = null; + String token = jwtProvider.resolveToken(request); + if (StrUtil.isNotBlank(token) && jwtProvider.checkToken(token)) { + auth = jwtProvider.getAuthentication(token); + if (Objects.nonNull(auth)) { + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + } catch (Exception e) { + ResponseWriteUtil.write(response, CommonResult.unauthorized()); + return; + } + filterChain.doFilter(req, res); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/MybatisPlusConfig.java b/src/main/java/com/zhangshu/chat/demo/config/MybatisPlusConfig.java new file mode 100644 index 0000000..2eaefd0 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/MybatisPlusConfig.java @@ -0,0 +1,32 @@ +package com.zhangshu.chat.demo.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MybatisPlusConfig + * + * @date 2022/3/4 18:59 + */ +@Configuration +@MapperScan(basePackages = "com.zhangshu.chat.demo.mapper") +public class MybatisPlusConfig { + /** + * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题 + */ +// @Bean +// public MybatisPlusInterceptor mybatisPlusInterceptor() { +// MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); +// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); +// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); +// return interceptor; +// } + +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/RestAuthenticationEntryPoint.java b/src/main/java/com/zhangshu/chat/demo/config/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..c2ae414 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/RestAuthenticationEntryPoint.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.config; + +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.util.ResponseWriteUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { + log.error(e.getMessage(), e); + ResponseWriteUtil.write(httpServletResponse, CommonResult.unauthorized()); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/RestfulAccessDeniedHandler.java b/src/main/java/com/zhangshu/chat/demo/config/RestfulAccessDeniedHandler.java new file mode 100644 index 0000000..85a846e --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/RestfulAccessDeniedHandler.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.config; + +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.util.ResponseWriteUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class RestfulAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { + log.error(e.getMessage(), e); + ResponseWriteUtil.write(httpServletResponse, CommonResult.unauthorized()); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/SecurityConfig.java b/src/main/java/com/zhangshu/chat/demo/config/SecurityConfig.java new file mode 100644 index 0000000..20627f5 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/SecurityConfig.java @@ -0,0 +1,95 @@ +package com.zhangshu.chat.demo.config; + +import com.zhangshu.chat.demo.service.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.Objects; + +/** + * SecurityConfig + * + * @date 2022/3/18 19:51 + */ +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + WhiteListHandler handler; + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + // 由于使用的是JWT,我们这里不需要csrf + .csrf() + .disable() + // 基于token,所以不需要session + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and(); + if (Objects.nonNull(handler)) { + handler.handle(httpSecurity); + } else { + httpSecurity + .authorizeRequests() + .anyRequest() + .authenticated() + .and(); + } + // 禁用缓存 + httpSecurity.headers().cacheControl(); + // 添加JWT filter + httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); + //添加自定义未授权和未登录结果返回 + httpSecurity.exceptionHandling() + .accessDeniedHandler(restfulAccessDeniedHandler()) + .authenticationEntryPoint(restAuthenticationEntryPoint()); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService()) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public RestfulAccessDeniedHandler restfulAccessDeniedHandler() { + return new RestfulAccessDeniedHandler(); + } + + @Bean + public RestAuthenticationEntryPoint restAuthenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + public JwtTokenAuthenticationFilter jwtAuthenticationTokenFilter() { + return new JwtTokenAuthenticationFilter(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + @Bean + public UserDetailsService userDetailsService() { + return new UserDetailsServiceImpl(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/SwaggerConfig.java b/src/main/java/com/zhangshu/chat/demo/config/SwaggerConfig.java new file mode 100644 index 0000000..b8a23da --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/SwaggerConfig.java @@ -0,0 +1,32 @@ +package com.zhangshu.chat.demo.config; + +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +@EnableKnife4j +public class SwaggerConfig { + + @Bean(value = "defaultApi2") + public Docket defaultApi2() { + Docket docket = new Docket(DocumentationType.SWAGGER_2) + .apiInfo(new ApiInfoBuilder() + .title("High City App") + .description("# High City App Api") + .version("1.0") + .build()) + //分组名称 + .select() + //这里指定Controller扫描包路径 + .apis(RequestHandlerSelectors.basePackage("com.zhangshu.chat.demo.controller")) + .paths(PathSelectors.any()) + .build(); + return docket; + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/UserDetailsImpl.java b/src/main/java/com/zhangshu/chat/demo/config/UserDetailsImpl.java new file mode 100644 index 0000000..ebfe6e4 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/UserDetailsImpl.java @@ -0,0 +1,41 @@ +package com.zhangshu.chat.demo.config; + +import lombok.Builder; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@Data +@Builder +public class UserDetailsImpl implements UserDetails { + private Long id; + private String username; + private String password; + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/UserInfo.java b/src/main/java/com/zhangshu/chat/demo/config/UserInfo.java new file mode 100644 index 0000000..6ebc195 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/UserInfo.java @@ -0,0 +1,15 @@ +package com.zhangshu.chat.demo.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfo { + private Long id; + private String username; +} diff --git a/src/main/java/com/zhangshu/chat/demo/config/WhiteListHandler.java b/src/main/java/com/zhangshu/chat/demo/config/WhiteListHandler.java new file mode 100644 index 0000000..61b0376 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/config/WhiteListHandler.java @@ -0,0 +1,37 @@ +package com.zhangshu.chat.demo.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +@Configuration +public class WhiteListHandler { + private final static String[] DEFAULT_WHITE_LIST = new String[]{ + "/", + "/*.html", + "/favicon.ico", + "/**/*.html", + "/**/*.css", + "/**/*.js", + "/swagger-resources/**", + "/swagger-ui/**", + "/v2/api-docs/**", + "/link", + "/auth/login", + "/event/**" + }; + + public void handle(HttpSecurity http) throws Exception { + http.authorizeRequests() + // 允许对于网站静态资源的无授权访问 + .antMatchers( + DEFAULT_WHITE_LIST + ) + .permitAll() + .antMatchers(HttpMethod.OPTIONS) + .permitAll() + .anyRequest() + .authenticated() + .and(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/constant/EAgoraEventType.java b/src/main/java/com/zhangshu/chat/demo/constant/EAgoraEventType.java new file mode 100644 index 0000000..3d8032c --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/constant/EAgoraEventType.java @@ -0,0 +1,25 @@ +package com.zhangshu.chat.demo.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EAgoraEventType { + + channel_create(101,"channel create","创建频道"), + channel_destroy(102,"channel destroy","销毁频道"), + broadcaster_join_channel(103,"broadcaster join channel","直播场景下,主播加入频道"), + broadcaster_leave_channel(104,"broadcaster leave channel","直播场景下,主播离开频道"), + audience_join_channel(105,"audience join channel","直播场景下,观众加入频道"), + audience_leave_channel(106,"audience leave channel","直播场景下,观众离开频道"), + user_join_channel_with_communication_mode(107,"user join channel with communication mode","通信场景下,用户加入频道"), + user_leave_channel_with_communication_mode(108,"user leave channel with communication mode","通信场景下,用户离开频道"), + client_role_change_to_broadcaster(111,"client role change to broadcaster","直播场景下,观众将角色切换为主播"), + client_role_change_to_audience(112,"client role change to audience","直播场景下,主播将角色切换为观众"), + ; + + private final int type; + private final String name; + private final String description; +} diff --git a/src/main/java/com/zhangshu/chat/demo/constant/ERoomUserType.java b/src/main/java/com/zhangshu/chat/demo/constant/ERoomUserType.java new file mode 100644 index 0000000..7880882 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/constant/ERoomUserType.java @@ -0,0 +1,7 @@ +package com.zhangshu.chat.demo.constant; + +public enum ERoomUserType { + broadcaster(), + audience(), + ; +} diff --git a/src/main/java/com/zhangshu/chat/demo/controller/EventController.java b/src/main/java/com/zhangshu/chat/demo/controller/EventController.java new file mode 100644 index 0000000..79f1e76 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/controller/EventController.java @@ -0,0 +1,31 @@ +package com.zhangshu.chat.demo.controller; + +import com.zhangshu.chat.demo.dto.AgoraEventDto; +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.dto.LoginDto; +import com.zhangshu.chat.demo.service.EventService; +import com.zhangshu.chat.demo.vo.LoginVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/event") +@Api(value = "EventController", tags = "事件回调") +public class EventController { + @Autowired + EventService eventService; + + @PostMapping("/agora") + @ApiOperation(value = "agora") + public CommonResult agora(@RequestBody AgoraEventDto dto) { + eventService.agora(dto); + return CommonResult.success(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/controller/LoginController.java b/src/main/java/com/zhangshu/chat/demo/controller/LoginController.java new file mode 100644 index 0000000..aa83900 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/controller/LoginController.java @@ -0,0 +1,45 @@ +package com.zhangshu.chat.demo.controller; + +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.dto.LoginDto; +import com.zhangshu.chat.demo.service.LoginService; +import com.zhangshu.chat.demo.vo.LoginVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.NonNull; +import org.hibernate.validator.constraints.Range; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * LoginController + * + * @date 2022/3/18 20:10 + */ +@RestController +@RequestMapping("/auth") +@Api(value = "LoginController", tags = "登陆授权") +public class LoginController { + + @Autowired + LoginService loginService; + + @PostMapping("/login") + @ApiOperation(value = "login") + public CommonResult login(@Valid @RequestBody LoginDto dto) { + LoginVo loginVo = loginService.login(dto); + return CommonResult.success(loginVo); + } + + @GetMapping("/agora/token") + @ApiOperation(value = "agora-token") + public CommonResult agoraToken(@Valid @NonNull Authentication authentication, + @RequestParam("channelName") String channelName, + @Range(min = 1, max = 2, message = "role error") @RequestParam("role") int role) { + LoginVo loginVo = loginService.agoraToken(authentication, channelName, role); + return CommonResult.success(loginVo); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/controller/RoomController.java b/src/main/java/com/zhangshu/chat/demo/controller/RoomController.java new file mode 100644 index 0000000..92be120 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/controller/RoomController.java @@ -0,0 +1,49 @@ +package com.zhangshu.chat.demo.controller; + +import com.zhangshu.chat.demo.dto.CommonPageResult; +import com.zhangshu.chat.demo.dto.CommonResult; +import com.zhangshu.chat.demo.dto.PageDto; +import com.zhangshu.chat.demo.dto.RoomCreateDto; +import com.zhangshu.chat.demo.service.RoomService; +import com.zhangshu.chat.demo.vo.RoomDetailVo; +import com.zhangshu.chat.demo.vo.RoomVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/room") +@Api(value = "RoomController", tags = "房间") +public class RoomController { + @Autowired + RoomService roomService; + + @GetMapping("/page") + public CommonResult> page(@Valid PageDto pageDto) { + CommonPageResult resp = roomService.page(pageDto); + return CommonResult.success(resp); + } + + @GetMapping("/detail/{id}") + public CommonResult detail(@PathVariable("id") String id) { + RoomDetailVo resp = roomService.detail(id); + return CommonResult.success(resp); + } + + @PostMapping("/create") + @ApiOperation(value = "房间 创建") + public CommonResult create(@Valid @RequestBody RoomCreateDto dto) { + String roomId = roomService.create(dto); + return CommonResult.success(roomId); + } + + @GetMapping("/create/{id}") + @ApiOperation(value = "房间 创建") + public CommonResult create(@PathVariable("id") String roomId) { + boolean success = roomService.create(roomId); + return CommonResult.success(success); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/convert/Convert.java b/src/main/java/com/zhangshu/chat/demo/convert/Convert.java new file mode 100644 index 0000000..6922d66 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/convert/Convert.java @@ -0,0 +1,7 @@ +package com.zhangshu.chat.demo.convert; + +import java.util.List; + +public interface Convert { + List convert(); +} diff --git a/src/main/java/com/zhangshu/chat/demo/convert/RoomConvert.java b/src/main/java/com/zhangshu/chat/demo/convert/RoomConvert.java new file mode 100644 index 0000000..1ab9b72 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/convert/RoomConvert.java @@ -0,0 +1,24 @@ +package com.zhangshu.chat.demo.convert; + +import com.zhangshu.chat.demo.entity.Room; +import com.zhangshu.chat.demo.vo.RoomDetailVo; +import com.zhangshu.chat.demo.vo.RoomVo; +import org.mapstruct.IterableMapping; +import org.mapstruct.Mapper; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface RoomConvert { + RoomConvert INSTANCE = Mappers.getMapper(RoomConvert.class); + + + RoomDetailVo convertDetail(Room records); + + RoomVo convert(Room records); + + @IterableMapping(elementTargetType = RoomVo.class) + List convert(List records); +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelEventDto.java b/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelEventDto.java new file mode 100644 index 0000000..83099f5 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelEventDto.java @@ -0,0 +1,55 @@ +package com.zhangshu.chat.demo.dto; + +import lombok.Data; + +@Data +public class AgoraChannelEventDto { + /** + * 频道名 + */ + private String channelName; + /** + * 该事件在 Agora 业务服务器上发生的 Unix 时间戳 (s)。 + */ + private Long ts; + + /** + * 观众在频道内的用户 ID。 + */ + private Long uid; + + /** + * 观众设备所属平台: + * 1:Android + * 2:iOS + * 5:Windows + * 6:Linux + * 7:Web + * 8:macOS + * 0:其他平台 + */ + private Long platform; + + /** + * Linux 平台的观众端使用的业务类型,常见的返回值包括: + * 3:本地服务端录制 + * 8:小程序 + * 10:云录制 + */ + private Long clientType; + + /** + * 序列号,标识该事件在 app 客户端上发生的顺序,可用于对同一用户的事件进行排序 + */ + private Long clientSeq; + + /** + * 离开频道的原因 + */ + private Long reason; + + /** + * 观众在频道内的时长 (s)。 + */ + private Long duration; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelUserEventDto.java b/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelUserEventDto.java new file mode 100644 index 0000000..d1077b1 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/AgoraChannelUserEventDto.java @@ -0,0 +1,15 @@ +package com.zhangshu.chat.demo.dto; + +import lombok.Data; + +@Data +public class AgoraChannelUserEventDto extends AgoraChannelEventDto{ + /** + * 频道名 + */ + private String channelName; + /** + * 该事件在 Agora 业务服务器上发生的 Unix 时间戳 (s)。 + */ + private Long ts; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/AgoraEventDto.java b/src/main/java/com/zhangshu/chat/demo/dto/AgoraEventDto.java new file mode 100644 index 0000000..2c724b0 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/AgoraEventDto.java @@ -0,0 +1,34 @@ +package com.zhangshu.chat.demo.dto; + +import cn.hutool.core.bean.BeanUtil; +import com.zhangshu.chat.demo.constant.EAgoraEventType; +import lombok.Data; + +@Data +public class AgoraEventDto { + /** + * 通知 ID,标识来自 Agora 业务服务器的一次事件通知 + */ + private String noticeId; + /** + * 业务 ID。值为 1 表示实时通信业务。 + */ + private Long productId; + /** + * 通知的事件类型 + */ + private EAgoraEventType eventType; + /** + * Agora 消息服务器向你的服务器发送事件通知的 Unix 时间戳 (ms)。通知重试时该值会更新。 + */ + private Long notifyMs; + /** + * 通知事件的具体内容。payload 因 eventType 而异 + */ + private Object payload; + + + public T convert(Class clazz){ + return BeanUtil.toBean(this.payload, clazz); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/CommonPageResult.java b/src/main/java/com/zhangshu/chat/demo/dto/CommonPageResult.java new file mode 100644 index 0000000..fcbc068 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/CommonPageResult.java @@ -0,0 +1,46 @@ +package com.zhangshu.chat.demo.dto; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.zhangshu.chat.demo.convert.Convert; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.List; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(value = "CommonPageResult", description = "通用分页返回") +public class CommonPageResult { + @ApiModelProperty("当前页") + private long page; + @ApiModelProperty("当前数量") + private long size; + @ApiModelProperty("当前总量") + private long total; + @ApiModelProperty("数据列表") + private List content; + + public static CommonPageResult of(Page page, Convert convert) { + return CommonPageResult.builder() + .total(page.getTotal()) + .page(page.getCurrent()) + .size(page.getSize()) + .content(convert.convert()) + .build(); + } + + public static CommonPageResult of(List data) { + return CommonPageResult.builder() + .total(data.size()) + .page(0) + .size(data.size()) + .content(data) + .build(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/CommonResult.java b/src/main/java/com/zhangshu/chat/demo/dto/CommonResult.java new file mode 100644 index 0000000..6a5b913 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/CommonResult.java @@ -0,0 +1,101 @@ +package com.zhangshu.chat.demo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(value = "CommonResult", description = "通用结果返回") +public class CommonResult { + @ApiModelProperty("状态码") + private Integer code; + @ApiModelProperty("操作信息") + private String message; + @ApiModelProperty("数据") + private T data; + + + public static CommonResult success(Integer code, String message, T data) { + return CommonResult.builder() + .code(code) + .message(message) + .data(data) + .build(); + } + + public static CommonResult success() { + return CommonResult.builder() + .code(HttpStatus.SUCCESS.getCode()) + .message(HttpStatus.SUCCESS.getMessage()) + .data(null) + .build(); + } + + public static CommonResult success(T data) { + return CommonResult.builder() + .code(HttpStatus.SUCCESS.getCode()) + .message(HttpStatus.SUCCESS.getMessage()) + .data(data) + .build(); + } + + public static CommonResult fail(Integer code, String message) { + return CommonResult.builder() + .code(code) + .message(message) + .data(null) + .build(); + } + + public static CommonResult fail(String message) { + return CommonResult.builder() + .code(HttpStatus.BAD_REQUEST.getCode()) + .message(message) + .data(null) + .build(); + } + + public static CommonResult unauthorized() { + return fail(HttpStatus.UNAUTHORIZED.getCode(), HttpStatus.UNAUTHORIZED.getMessage()); + } + + public static CommonResult forbidden() { + return fail(HttpStatus.FORBIDDEN.getCode(), HttpStatus.FORBIDDEN.getMessage()); + } + + public static CommonResult unauthorized(String message) { + return fail(HttpStatus.UNAUTHORIZED.getCode(), message); + } + + public static CommonResult forbidden(String message) { + return fail(HttpStatus.FORBIDDEN.getCode(), message); + } + + @Getter + @AllArgsConstructor + public enum HttpStatus { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(402, "参数检验失败"), + BAD_REQUEST(400, "请求参数错误"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + AUTHORIZATION_HEADER_IS_EMPTY(600, "请求头中的token为空"), + GET_TOKEN_KEY_ERROR(601, "远程获取TokenKey异常"), + GEN_PUBLIC_KEY_ERROR(602, "生成公钥异常"), + JWT_TOKEN_EXPIRE(603, "token校验异常"), + TOMANY_REQUEST_ERROR(429, "后端服务触发流控"), + BACKGROUD_DEGRADE_ERROR(604, "后端服务触发降级"), + BAD_GATEWAY(502, "网关服务异常"), + FORBIDDEN(403, "没有相关权限"); + + private final int code; + private final String message; + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/IdDto.java b/src/main/java/com/zhangshu/chat/demo/dto/IdDto.java new file mode 100644 index 0000000..e9f8fb2 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/IdDto.java @@ -0,0 +1,12 @@ +package com.zhangshu.chat.demo.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdDto { + private Long id; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/LoginDto.java b/src/main/java/com/zhangshu/chat/demo/dto/LoginDto.java new file mode 100644 index 0000000..aa225cf --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/LoginDto.java @@ -0,0 +1,23 @@ +package com.zhangshu.chat.demo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * LoginDto + * + * @date 2022/3/18 20:21 + */ +@Data +@ApiModel(value = "LoginDto", description = "登陆") +public class LoginDto { + @NotBlank(message = "用户名 不能为空") + @ApiModelProperty(name = "username", value = "用户名", required = true) + private String username; + @NotBlank(message = "password 不能为空") + @ApiModelProperty(name = "password", value = "password", required = true) + private String password; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/PageDto.java b/src/main/java/com/zhangshu/chat/demo/dto/PageDto.java new file mode 100644 index 0000000..d75e9a4 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/PageDto.java @@ -0,0 +1,23 @@ +package com.zhangshu.chat.demo.dto; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.Min; + +@Data +@ApiModel(value = "PageDto", description = "分页参数") +public class PageDto { + @Min(value = 1, message = "最小页码不能小于1") + @ApiModelProperty(name = "page", value = "页码", required = true) + private Long page = 1L; + @Min(value = 1, message = "最小页数不能小于1") + @ApiModelProperty(name = "size", value = "页数", required = true) + private Long size = 10L; + + public Page of() { + return new Page(page, size); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/RegisterDto.java b/src/main/java/com/zhangshu/chat/demo/dto/RegisterDto.java new file mode 100644 index 0000000..9adbb95 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/RegisterDto.java @@ -0,0 +1,26 @@ +package com.zhangshu.chat.demo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * RegisterDto + * + * @date 2022/3/18 20:21 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@ApiModel(value = "RegisterDto", description = "注册dto") +public class RegisterDto extends LoginDto { + @NotBlank(message = "名称 不能为空") + @ApiModelProperty(name = "name",value = "名称",required = true) + private String name; + + @NotBlank(message = "验证码 不能为空") + @ApiModelProperty(name = "code",value = "验证码",required = true) + private String code; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/Resource.java b/src/main/java/com/zhangshu/chat/demo/dto/Resource.java new file mode 100644 index 0000000..3dfffda --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/Resource.java @@ -0,0 +1,20 @@ +package com.zhangshu.chat.demo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(value = "Resource", description = "图片资源") +public class Resource { + @ApiModelProperty(name = "path", value = "存储路径") + private String path; + @ApiModelProperty(name = "url", value = "访问url") + private String url; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/RoomCreateDto.java b/src/main/java/com/zhangshu/chat/demo/dto/RoomCreateDto.java new file mode 100644 index 0000000..43fa7da --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/RoomCreateDto.java @@ -0,0 +1,8 @@ +package com.zhangshu.chat.demo.dto; + +import lombok.Data; + +@Data +public class RoomCreateDto { + private String name; +} diff --git a/src/main/java/com/zhangshu/chat/demo/dto/TimeDto.java b/src/main/java/com/zhangshu/chat/demo/dto/TimeDto.java new file mode 100644 index 0000000..5864846 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/dto/TimeDto.java @@ -0,0 +1,20 @@ +package com.zhangshu.chat.demo.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class TimeDto extends IdDto { + // @JsonSerialize(using = TimeStampToDateSerializer.class) + @ApiModelProperty(name = "createdAt", value = "创建时间") + private Long createdAt; + // @JsonSerialize(using = TimeStampToDateSerializer.class) + @ApiModelProperty(name = "updatedAt", value = "更新时间") + private Long updatedAt; +} diff --git a/src/main/java/com/zhangshu/chat/demo/entity/Room.java b/src/main/java/com/zhangshu/chat/demo/entity/Room.java new file mode 100644 index 0000000..7c49bdf --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/entity/Room.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.entity; + +import com.zhangshu.chat.demo.vo.RoomUserVo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Room { + private String id; + private String name; + @Builder.Default + private List userList = new ArrayList<>(); +} diff --git a/src/main/java/com/zhangshu/chat/demo/entity/User.java b/src/main/java/com/zhangshu/chat/demo/entity/User.java new file mode 100644 index 0000000..8698552 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/entity/User.java @@ -0,0 +1,12 @@ +package com.zhangshu.chat.demo.entity; + +import lombok.Data; + +@Data +public class User { + private Long id; + private String username; + private String nickname; + private String email; + private String password; +} diff --git a/src/main/java/com/zhangshu/chat/demo/mapper/UserMapper.java b/src/main/java/com/zhangshu/chat/demo/mapper/UserMapper.java new file mode 100644 index 0000000..0796ea3 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.zhangshu.chat.demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.zhangshu.chat.demo.entity.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/EventService.java b/src/main/java/com/zhangshu/chat/demo/service/EventService.java new file mode 100644 index 0000000..506b50e --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/EventService.java @@ -0,0 +1,7 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.dto.AgoraEventDto; + +public interface EventService { + void agora(AgoraEventDto dto); +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/EventServiceImpl.java b/src/main/java/com/zhangshu/chat/demo/service/EventServiceImpl.java new file mode 100644 index 0000000..c79ed36 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/EventServiceImpl.java @@ -0,0 +1,87 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.config.AgoraProperties; +import com.zhangshu.chat.demo.constant.ERoomUserType; +import com.zhangshu.chat.demo.dto.AgoraChannelEventDto; +import com.zhangshu.chat.demo.dto.AgoraChannelUserEventDto; +import com.zhangshu.chat.demo.dto.AgoraEventDto; +import com.zhangshu.chat.demo.entity.Room; +import com.zhangshu.chat.demo.entity.User; +import com.zhangshu.chat.demo.mapper.UserMapper; +import com.zhangshu.chat.demo.vo.RoomUserVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +@EnableConfigurationProperties(AgoraProperties.class) +public class EventServiceImpl implements EventService { + @Autowired + AgoraProperties agoraProperties; + @Autowired + RoomCache roomCache; + @Autowired + UserMapper userMapper; + + @Override + public void agora(AgoraEventDto dto) { + switch (dto.getEventType()) { + case channel_create: { + AgoraChannelEventDto eventDto = dto.convert(AgoraChannelEventDto.class); + roomCache.add(eventDto.getChannelName()); + } + break; + case channel_destroy: { + AgoraChannelEventDto eventDto = dto.convert(AgoraChannelEventDto.class); + roomCache.remove(eventDto.getChannelName()); + } + break; + case broadcaster_join_channel: { + AgoraChannelUserEventDto eventDto = dto.convert(AgoraChannelUserEventDto.class); + User user = userMapper.selectById(eventDto.getUid()); + if (Objects.isNull(user)) { + break; + } + roomCache.addUser(eventDto.getChannelName(), RoomUserVo.builder() + .id(eventDto.getUid()) + .nickname(user.getNickname()) + .type(ERoomUserType.broadcaster) + .lastClientSeq(eventDto.getClientSeq()) + .build()); + } + break; + case broadcaster_leave_channel: + case audience_leave_channel: { + AgoraChannelUserEventDto eventDto = dto.convert(AgoraChannelUserEventDto.class); + roomCache.removeUser(eventDto.getChannelName(), eventDto.getUid()); + } + break; + case audience_join_channel: { + AgoraChannelUserEventDto eventDto = dto.convert(AgoraChannelUserEventDto.class); + User user = userMapper.selectById(eventDto.getUid()); + if (Objects.isNull(user)) { + break; + } + roomCache.addUser(eventDto.getChannelName(), RoomUserVo.builder() + .id(eventDto.getUid()) + .nickname(user.getNickname()) + .type(ERoomUserType.audience) + .lastClientSeq(eventDto.getClientSeq()) + .build()); + } + break; + case client_role_change_to_audience: { + AgoraChannelUserEventDto eventDto = dto.convert(AgoraChannelUserEventDto.class); + roomCache.changeUserType(eventDto.getChannelName(), eventDto.getUid(), ERoomUserType.audience); + } + break; + case client_role_change_to_broadcaster: { + AgoraChannelUserEventDto eventDto = dto.convert(AgoraChannelUserEventDto.class); + roomCache.changeUserType(eventDto.getChannelName(), eventDto.getUid(), ERoomUserType.broadcaster); + } + break; + } + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/LoginService.java b/src/main/java/com/zhangshu/chat/demo/service/LoginService.java new file mode 100644 index 0000000..fca101c --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/LoginService.java @@ -0,0 +1,13 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.dto.LoginDto; +import com.zhangshu.chat.demo.vo.LoginVo; +import org.springframework.security.core.Authentication; + + +public interface LoginService { + + LoginVo login(LoginDto dto); + + LoginVo agoraToken(Authentication authentication, String channelName, int role); +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/LoginServiceImpl.java b/src/main/java/com/zhangshu/chat/demo/service/LoginServiceImpl.java new file mode 100644 index 0000000..45bcb33 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/LoginServiceImpl.java @@ -0,0 +1,65 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.config.*; +import com.zhangshu.chat.demo.dto.LoginDto; +import com.zhangshu.chat.demo.vo.LoginVo; +import io.agora.media.RtcTokenBuilder2; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + + +@Slf4j +@Service +@EnableConfigurationProperties(AgoraProperties.class) +public class LoginServiceImpl implements LoginService { + @Autowired + AgoraProperties agoraProperties; + + @Autowired + JwtProvider jwtProvider; + @Autowired + AuthenticationManager authenticationManager; + + + @Override + public LoginVo login(LoginDto dto) { + UserDetails userDetails; + try { + Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword())); + userDetails = (UserDetails) authenticate.getPrincipal(); + } catch (BadCredentialsException e) { + throw new BusinessException("账号或者密码错误"); + } + Long id = ((UserDetailsImpl) userDetails).getId(); + return LoginVo.builder() + .id(id) + .token(jwtProvider.createToken(UserInfo.builder() + .id(id) + .username(userDetails.getUsername()) + .build())) + .username(userDetails.getUsername()) + .build(); + } + + @Override + public LoginVo agoraToken(Authentication authentication, String channelName, int role) { + RtcTokenBuilder2.Role role2 = role == 1 ? RtcTokenBuilder2.Role.ROLE_PUBLISHER : RtcTokenBuilder2.Role.ROLE_SUBSCRIBER; + UserDetailsImpl userInfo = (UserDetailsImpl) authentication.getPrincipal(); + RtcTokenBuilder2 token = new RtcTokenBuilder2(); + String result = token.buildTokenWithUid(agoraProperties.getAppId(), agoraProperties.getAppCertificate(), + channelName, userInfo.getId().intValue(), role2, + agoraProperties.getTokenExpiration(), agoraProperties.getPrivilegeExpiration()); + return LoginVo.builder() + .token(result).build(); + } + + +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/RoomCache.java b/src/main/java/com/zhangshu/chat/demo/service/RoomCache.java new file mode 100644 index 0000000..097c052 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/RoomCache.java @@ -0,0 +1,79 @@ +package com.zhangshu.chat.demo.service; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.CacheUtil; +import cn.hutool.core.lang.UUID; +import com.zhangshu.chat.demo.constant.ERoomUserType; +import com.zhangshu.chat.demo.entity.Room; +import com.zhangshu.chat.demo.vo.RoomUserVo; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Component +public class RoomCache { + private final Cache roomCache = CacheUtil.newFIFOCache(4); + /** + * 正在创建 + */ + private final Cache roomCreatingCache = CacheUtil.newFIFOCache(4); + + public List list() { + List resp = new ArrayList<>(roomCache.size()); + roomCache.forEach(resp::add); + return resp; + } + + public synchronized Room addCreating(String roomName) { + Room room = Room.builder() + .id(UUID.fastUUID().toString()) + .name(roomName) + .build(); + roomCreatingCache.put(room.getId(), room); + return room; + } + + public boolean createSuccess(String roomId) { + return roomCache.containsKey(roomId); + } + + public Room get(String roomId) { + return roomCache.get(roomId); + } + + public synchronized void add(String roomId) { + Room room = roomCreatingCache.get(roomId); + if (Objects.isNull(room)) { + return; + } + roomCache.put(roomId, room); + roomCreatingCache.remove(roomId); + } + + public synchronized void addUser(String roomId, RoomUserVo user) { + Room room = roomCache.get(roomId); + if (Objects.nonNull(room)) { + room.getUserList().add(user); + } + } + + public void remove(String roomId) { + roomCache.remove(roomId); + } + + public synchronized void removeUser(String roomId, Long userId) { + Room room = roomCache.get(roomId); + if (Objects.nonNull(room)) { + room.getUserList().stream().filter(v -> v.getId().equals(userId)).findFirst().ifPresent(v -> room.getUserList().remove(v)); + } + } + + public void changeUserType(String roomId, Long userId, ERoomUserType type) { + Room room = roomCache.get(roomId); + if (Objects.nonNull(room)) { + room.getUserList().stream().filter(v -> v.getId().equals(userId)).findFirst().ifPresent(v -> v.setType(type)); + } + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/RoomService.java b/src/main/java/com/zhangshu/chat/demo/service/RoomService.java new file mode 100644 index 0000000..84f165c --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/RoomService.java @@ -0,0 +1,17 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.dto.CommonPageResult; +import com.zhangshu.chat.demo.dto.PageDto; +import com.zhangshu.chat.demo.dto.RoomCreateDto; +import com.zhangshu.chat.demo.vo.RoomDetailVo; +import com.zhangshu.chat.demo.vo.RoomVo; + +public interface RoomService { + CommonPageResult page(PageDto pageDto); + + RoomDetailVo detail(String id); + + String create(RoomCreateDto dto); + + boolean create(String roomId); +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/RoomServiceImpl.java b/src/main/java/com/zhangshu/chat/demo/service/RoomServiceImpl.java new file mode 100644 index 0000000..39d13b9 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/RoomServiceImpl.java @@ -0,0 +1,42 @@ +package com.zhangshu.chat.demo.service; + +import com.zhangshu.chat.demo.convert.RoomConvert; +import com.zhangshu.chat.demo.dto.CommonPageResult; +import com.zhangshu.chat.demo.dto.PageDto; +import com.zhangshu.chat.demo.dto.RoomCreateDto; +import com.zhangshu.chat.demo.entity.Room; +import com.zhangshu.chat.demo.vo.RoomDetailVo; +import com.zhangshu.chat.demo.vo.RoomVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class RoomServiceImpl implements RoomService { + @Autowired + RoomCache roomCache; + + + @Override + public CommonPageResult page(PageDto pageDto) { + return CommonPageResult.of(RoomConvert.INSTANCE.convert(roomCache.list())); + } + + @Override + public RoomDetailVo detail(String id) { + return RoomConvert.INSTANCE.convertDetail(roomCache.get(id)); + } + + @Override + public String create(RoomCreateDto dto) { + Room room = roomCache.addCreating(dto.getName()); + return room.getId(); + } + + @Override + public boolean create(String roomId) { + return roomCache.createSuccess(roomId); + } + +} diff --git a/src/main/java/com/zhangshu/chat/demo/service/UserDetailsServiceImpl.java b/src/main/java/com/zhangshu/chat/demo/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..7c09db1 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/service/UserDetailsServiceImpl.java @@ -0,0 +1,39 @@ +package com.zhangshu.chat.demo.service; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.zhangshu.chat.demo.mapper.UserMapper; +import com.zhangshu.chat.demo.config.UserDetailsImpl; +import com.zhangshu.chat.demo.entity.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/** + * UserDetailsServiceImpl + * + * @date 2022/3/18 20:35 + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userMapper.selectOne(Wrappers.lambdaQuery().eq(User::getUsername, username)); + if (Objects.isNull(user)) { + throw new UsernameNotFoundException("user not found"); + } + return UserDetailsImpl.builder() + .id(user.getId()) + .username(user.getUsername()) + .password(new BCryptPasswordEncoder().encode(user.getPassword())) + .build(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/util/IpAddressUtil.java b/src/main/java/com/zhangshu/chat/demo/util/IpAddressUtil.java new file mode 100644 index 0000000..4e5ef4e --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/util/IpAddressUtil.java @@ -0,0 +1,29 @@ +package com.zhangshu.chat.demo.util; + + +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +public class IpAddressUtil { + + public static String getIpAddress(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/util/ResponseWriteUtil.java b/src/main/java/com/zhangshu/chat/demo/util/ResponseWriteUtil.java new file mode 100644 index 0000000..1871ab5 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/util/ResponseWriteUtil.java @@ -0,0 +1,21 @@ +package com.zhangshu.chat.demo.util; + +import cn.hutool.json.JSONUtil; +import com.zhangshu.chat.demo.dto.CommonResult; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ResponseWriteUtil { + public static void write(HttpServletResponse httpServletResponse, CommonResult result) throws IOException { + httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); + httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true"); + httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS"); + httpServletResponse.setHeader("Access-Control-Max-Age", "3600"); + httpServletResponse.setHeader("Access-Control-Allow-Headers", "*"); + httpServletResponse.setCharacterEncoding("utf-8"); + httpServletResponse.setContentType("text/javascript;charset=utf-8"); + httpServletResponse.getWriter().println(JSONUtil.toJsonStr(result)); + httpServletResponse.getWriter().flush(); + } +} diff --git a/src/main/java/com/zhangshu/chat/demo/vo/LoginVo.java b/src/main/java/com/zhangshu/chat/demo/vo/LoginVo.java new file mode 100644 index 0000000..c8d6e20 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/vo/LoginVo.java @@ -0,0 +1,22 @@ +package com.zhangshu.chat.demo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(value = "LoginVo", description = "登录返回") +public class LoginVo { + @ApiModelProperty(value = "id", name = "id") + private Long id; + @ApiModelProperty(value = "token", name = "token") + private String token; + @ApiModelProperty(value = "username", name = "username") + private String username; +} diff --git a/src/main/java/com/zhangshu/chat/demo/vo/RoomDetailVo.java b/src/main/java/com/zhangshu/chat/demo/vo/RoomDetailVo.java new file mode 100644 index 0000000..449111c --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/vo/RoomDetailVo.java @@ -0,0 +1,13 @@ +package com.zhangshu.chat.demo.vo; + +import com.zhangshu.chat.demo.entity.User; +import lombok.Data; + +import java.util.List; + +@Data +public class RoomDetailVo { + private Long id; + private String name; + private List userList; +} diff --git a/src/main/java/com/zhangshu/chat/demo/vo/RoomUserVo.java b/src/main/java/com/zhangshu/chat/demo/vo/RoomUserVo.java new file mode 100644 index 0000000..de7c559 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/vo/RoomUserVo.java @@ -0,0 +1,19 @@ +package com.zhangshu.chat.demo.vo; + +import com.zhangshu.chat.demo.constant.ERoomUserType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RoomUserVo { + private Long id; + private String username; + private String nickname; + private ERoomUserType type; + private Long lastClientSeq; +} diff --git a/src/main/java/com/zhangshu/chat/demo/vo/RoomVo.java b/src/main/java/com/zhangshu/chat/demo/vo/RoomVo.java new file mode 100644 index 0000000..d23aa75 --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/vo/RoomVo.java @@ -0,0 +1,9 @@ +package com.zhangshu.chat.demo.vo; + +import lombok.Data; + +@Data +public class RoomVo { + private Long id; + private String name; +} diff --git a/src/main/java/com/zhangshu/chat/demo/vo/UserInfoVo.java b/src/main/java/com/zhangshu/chat/demo/vo/UserInfoVo.java new file mode 100644 index 0000000..e519f5b --- /dev/null +++ b/src/main/java/com/zhangshu/chat/demo/vo/UserInfoVo.java @@ -0,0 +1,25 @@ +package com.zhangshu.chat.demo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +@Data +@ApiModel(value = "UserInfoVo", description = "用户信息") +public class UserInfoVo implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(name = "id", value = "user id") + private Long id; + + @ApiModelProperty(name = "username", value = "用户名") + private String username; + + @ApiModelProperty(name = "nickname", value = "昵称") + private String nickname; + + @ApiModelProperty(name = "email", value = "邮箱") + private String email; +} diff --git a/src/main/java/io/agora/chat/ChatTokenBuilder2.java b/src/main/java/io/agora/chat/ChatTokenBuilder2.java new file mode 100644 index 0000000..5a7ee15 --- /dev/null +++ b/src/main/java/io/agora/chat/ChatTokenBuilder2.java @@ -0,0 +1,60 @@ +package io.agora.chat; + +import io.agora.media.AccessToken2; + +public class ChatTokenBuilder2 { + + /** + * Build the CHAT user token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param userId: The user's id, must be unique. + * optionalUid must be unique. + * @param expire: represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The RTC token. + */ + public String buildUserToken(String appId, String appCertificate, String userId, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + AccessToken2.Service serviceChat = new AccessToken2.ServiceChat(userId); + + serviceChat.addPrivilegeChat(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_USER, expire); + accessToken.addService(serviceChat); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + /** + * Build the CHAT app token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param expire: represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The RTC token. + */ + public String buildAppToken(String appId, String appCertificate, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + AccessToken2.Service serviceChat = new AccessToken2.ServiceChat(); + + serviceChat.addPrivilegeChat(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_APP, expire); + accessToken.addService(serviceChat); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/education/EducationTokenBuilder2.java b/src/main/java/io/agora/education/EducationTokenBuilder2.java new file mode 100644 index 0000000..a42eb57 --- /dev/null +++ b/src/main/java/io/agora/education/EducationTokenBuilder2.java @@ -0,0 +1,98 @@ +package io.agora.education; + +import io.agora.media.AccessToken2; +import io.agora.media.Utils; + +public class EducationTokenBuilder2 { + + /** + * build user room token + * + * @param appId The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param roomUuid The room's id, must be unique. + * @param userUuid The user's id, must be unique. + * @param role The user's role, such as 0(invisible), 1(teacher), 2(student), 3(assistant), 4(observer) etc. + * @param expire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The education user room token. + */ + public String buildRoomUserToken(String appId, String appCertificate, String roomUuid, String userUuid, Short role, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + String chatUserId = Utils.md5(userUuid); + + AccessToken2.Service serviceEducation = new AccessToken2.ServiceEducation(roomUuid, userUuid, role); + serviceEducation.addPrivilegeEducation(AccessToken2.PrivilegeEducation.PRIVILEGE_ROOM_USER, expire); + accessToken.addService(serviceEducation); + + AccessToken2.Service serviceRtm = new AccessToken2.ServiceRtm(userUuid); + serviceRtm.addPrivilegeRtm(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN, expire); + accessToken.addService(serviceRtm); + + AccessToken2.Service serviceChat = new AccessToken2.ServiceChat(chatUserId); + serviceRtm.addPrivilegeChat(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_USER, expire); + accessToken.addService(serviceChat); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + /** + * build user individual token + * + * @param appId The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param userUuid The user's id, must be unique. + * @param expire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The education user token. + */ + public String buildUserToken(String appId, String appCertificate, String userUuid, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + AccessToken2.Service service = new AccessToken2.ServiceEducation(userUuid); + + service.addPrivilegeEducation(AccessToken2.PrivilegeEducation.PRIVILEGE_USER, expire); + accessToken.addService(service); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + /** + * build app global token + * + * @param appId The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param expire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The education global token. + */ + public String buildAppToken(String appId, String appCertificate, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + AccessToken2.Service serviceEducation = new AccessToken2.ServiceEducation(); + + serviceEducation.addPrivilegeEducation(AccessToken2.PrivilegeEducation.PRIVILEGE_APP, expire); + accessToken.addService(serviceEducation); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/media/AccessToken.java b/src/main/java/io/agora/media/AccessToken.java new file mode 100644 index 0000000..9677435 --- /dev/null +++ b/src/main/java/io/agora/media/AccessToken.java @@ -0,0 +1,168 @@ +package io.agora.media; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.TreeMap; + +import static io.agora.media.Utils.crc32; + +public class AccessToken { + public enum Privileges { + kJoinChannel(1), + kPublishAudioStream(2), + kPublishVideoStream(3), + kPublishDataStream(4), + + // For RTM only + kRtmLogin(1000); + + public short intValue; + + Privileges(int value) { + intValue = (short) value; + } + } + + private static final String VER = "006"; + + public String appId; + public String appCertificate; + public String channelName; + public String uid; + public byte[] signature; + public byte[] messageRawContent; + public int crcChannelName; + public int crcUid; + public PrivilegeMessage message; + public int expireTimestamp; + + public AccessToken(String appId, String appCertificate, String channelName, String uid) { + this.appId = appId; + this.appCertificate = appCertificate; + this.channelName = channelName; + this.uid = uid; + this.crcChannelName = 0; + this.crcUid = 0; + this.message = new PrivilegeMessage(); + } + + public String build() throws Exception { + if (! Utils.isUUID(appId)) { + return ""; + } + + if (!Utils.isUUID(appCertificate)) { + return ""; + } + + messageRawContent = Utils.pack(message); + signature = generateSignature(appCertificate, + appId, channelName, uid, messageRawContent); + crcChannelName = crc32(channelName); + crcUid = crc32(uid); + + PackContent packContent = new PackContent(signature, crcChannelName, crcUid, messageRawContent); + byte[] content = Utils.pack(packContent); + return getVersion() + this.appId + Utils.base64Encode(content); + } + + public void addPrivilege(Privileges privilege, int expireTimestamp) { + message.messages.put(privilege.intValue, expireTimestamp); + } + + public static String getVersion() { + return VER; + } + + public static byte[] generateSignature(String appCertificate, + String appID, String channelName, String uid, byte[] message) throws Exception { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + baos.write(appID.getBytes()); + baos.write(channelName.getBytes()); + baos.write(uid.getBytes()); + baos.write(message); + } catch (IOException e) { + e.printStackTrace(); + } + return Utils.hmacSign(appCertificate, baos.toByteArray()); + } + + public boolean fromString(String token) { + if (!getVersion().equals(token.substring(0, Utils.VERSION_LENGTH))) { + return false; + } + + try { + appId = token.substring(Utils.VERSION_LENGTH, Utils.VERSION_LENGTH + Utils.APP_ID_LENGTH); + PackContent packContent = new PackContent(); + Utils.unpack(Utils.base64Decode(token.substring(Utils.VERSION_LENGTH + Utils.APP_ID_LENGTH, token.length())), packContent); + signature = packContent.signature; + crcChannelName = packContent.crcChannelName; + crcUid = packContent.crcUid; + messageRawContent = packContent.rawMessage; + Utils.unpack(messageRawContent, message); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + public class PrivilegeMessage implements PackableEx { + public int salt; + public int ts; + public TreeMap messages; + + public PrivilegeMessage() { + salt = Utils.randomInt(); + ts = Utils.getTimestamp() + 24 * 3600; + messages = new TreeMap<>(); + } + + @Override + public ByteBuf marshal(ByteBuf out) { + return out.put(salt).put(ts).putIntMap(messages); + } + + @Override + public void unmarshal(ByteBuf in) { + salt = in.readInt(); + ts = in.readInt(); + messages = in.readIntMap(); + } + } + + public class PackContent implements PackableEx { + public byte[] signature; + public int crcChannelName; + public int crcUid; + public byte[] rawMessage; + + public PackContent() { + // Nothing done + } + + public PackContent(byte[] signature, int crcChannelName, int crcUid, byte[] rawMessage) { + this.signature = signature; + this.crcChannelName = crcChannelName; + this.crcUid = crcUid; + this.rawMessage = rawMessage; + } + + @Override + public ByteBuf marshal(ByteBuf out) { + return out.put(signature).put(crcChannelName).put(crcUid).put(rawMessage); + } + + @Override + public void unmarshal(ByteBuf in) { + signature = in.readBytes(); + crcChannelName = in.readInt(); + crcUid = in.readInt(); + rawMessage = in.readBytes(); + } + } +} diff --git a/src/main/java/io/agora/media/AccessToken2.java b/src/main/java/io/agora/media/AccessToken2.java new file mode 100644 index 0000000..2897d26 --- /dev/null +++ b/src/main/java/io/agora/media/AccessToken2.java @@ -0,0 +1,380 @@ +package io.agora.media; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Map; +import java.util.TreeMap; + +public class AccessToken2 { + public enum PrivilegeRtc { + PRIVILEGE_JOIN_CHANNEL(1), + PRIVILEGE_PUBLISH_AUDIO_STREAM(2), + PRIVILEGE_PUBLISH_VIDEO_STREAM(3), + PRIVILEGE_PUBLISH_DATA_STREAM(4), + ; + + public short intValue; + + PrivilegeRtc(int value) { + intValue = (short) value; + } + } + + public enum PrivilegeRtm { + PRIVILEGE_LOGIN(1), + ; + + public short intValue; + + PrivilegeRtm(int value) { + intValue = (short) value; + } + } + + public enum PrivilegeFpa { + PRIVILEGE_LOGIN(1), + ; + + public short intValue; + + PrivilegeFpa(int value) { + intValue = (short) value; + } + } + + public enum PrivilegeChat { + PRIVILEGE_CHAT_USER(1), + PRIVILEGE_CHAT_APP(2), + ; + + public short intValue; + + PrivilegeChat(int value) { + intValue = (short) value; + } + } + + public enum PrivilegeEducation { + PRIVILEGE_ROOM_USER(1), + PRIVILEGE_USER(2), + PRIVILEGE_APP(3), + ; + + public short intValue; + + PrivilegeEducation(int value) { + intValue = (short) value; + } + } + + private static final String VERSION = "007"; + public static final short SERVICE_TYPE_RTC = 1; + public static final short SERVICE_TYPE_RTM = 2; + public static final short SERVICE_TYPE_FPA = 4; + public static final short SERVICE_TYPE_CHAT = 5; + public static final short SERVICE_TYPE_EDUCATION = 7; + + public String appCert = ""; + public String appId = ""; + public int expire; + public int issueTs; + public int salt; + public Map services = new TreeMap<>(); + + public AccessToken2() { + } + + public AccessToken2(String appId, String appCert, int expire) { + this.appCert = appCert; + this.appId = appId; + this.expire = expire; + this.issueTs = Utils.getTimestamp(); + this.salt = Utils.randomInt(); + } + + public void addService(Service service) { + this.services.put(service.getServiceType(), service); + } + + public String build() throws Exception { + if (!Utils.isUUID(this.appId) || !Utils.isUUID(this.appCert)) { + return ""; + } + + ByteBuf buf = new ByteBuf().put(this.appId).put(this.issueTs).put(this.expire).put(this.salt).put((short) this.services.size()); + byte[] signing = getSign(); + + this.services.forEach((k, v) -> { + v.pack(buf); + }); + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(signing, "HmacSHA256")); + byte[] signature = mac.doFinal(buf.asBytes()); + + ByteBuf bufferContent = new ByteBuf(); + bufferContent.put(signature); + bufferContent.buffer.put(buf.asBytes()); + + return getVersion() + Utils.base64Encode(Utils.compress(bufferContent.asBytes())); + } + + public Service getService(short serviceType) { + if (serviceType == SERVICE_TYPE_RTC) { + return new ServiceRtc(); + } + if (serviceType == SERVICE_TYPE_RTM) { + return new ServiceRtm(); + } + if (serviceType == SERVICE_TYPE_FPA) { + return new ServiceFpa(); + } + if (serviceType == SERVICE_TYPE_CHAT) { + return new ServiceChat(); + } + if (serviceType == SERVICE_TYPE_EDUCATION) { + return new ServiceEducation(); + } + throw new IllegalArgumentException(String.format("unknown service type: `%d`", serviceType)); + } + + public byte[] getSign() throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(new ByteBuf().put(this.issueTs).asBytes(), "HmacSHA256")); + byte[] signing = mac.doFinal(this.appCert.getBytes()); + mac.init(new SecretKeySpec(new ByteBuf().put(this.salt).asBytes(), "HmacSHA256")); + return mac.doFinal(signing); + } + + public static String getUidStr(int uid) { + if (uid == 0) { + return ""; + } + return String.valueOf(uid & 0xFFFFFFFFL); + } + + public static String getVersion() { + return VERSION; + } + + public boolean parse(String token) { + if (!getVersion().equals(token.substring(0, Utils.VERSION_LENGTH))) { + return false; + } + byte[] data = Utils.decompress(Utils.base64Decode(token.substring(Utils.VERSION_LENGTH))); + ByteBuf buff = new ByteBuf(data); + String signature = buff.readString(); + this.appId = buff.readString(); + this.issueTs = buff.readInt(); + this.expire = buff.readInt(); + this.salt = buff.readInt(); + short servicesNum = buff.readShort(); + + for (int i = 0; i < servicesNum; i++) { + short serviceType = buff.readShort(); + Service service = getService(serviceType); + service.unpack(buff); + this.services.put(serviceType, service); + } + return true; + } + + public static class Service { + public short type; + public TreeMap privileges = new TreeMap() { + }; + + public Service() { + } + + public Service(short serviceType) { + this.type = serviceType; + } + + public void addPrivilegeRtc(PrivilegeRtc privilege, int expire) { + this.privileges.put(privilege.intValue, expire); + } + + public void addPrivilegeRtm(PrivilegeRtm privilege, int expire) { + this.privileges.put(privilege.intValue, expire); + } + + public void addPrivilegeFpa(PrivilegeFpa privilege, int expire) { + this.privileges.put(privilege.intValue, expire); + } + + public void addPrivilegeChat(PrivilegeChat privilege, int expire) { + this.privileges.put(privilege.intValue, expire); + } + + public void addPrivilegeEducation(PrivilegeEducation privilege, int expire) { + this.privileges.put(privilege.intValue, expire); + } + + public TreeMap getPrivileges() { + return this.privileges; + } + + public short getServiceType() { + return this.type; + } + + public ByteBuf pack(ByteBuf buf) { + return buf.put(this.type).putIntMap(this.privileges); + } + + public void unpack(ByteBuf byteBuf) { + this.privileges = byteBuf.readIntMap(); + } + } + + public static class ServiceRtc extends Service { + public String channelName; + public String uid; + + public ServiceRtc() { + this.type = SERVICE_TYPE_RTC; + } + + public ServiceRtc(String channelName, String uid) { + this.type = SERVICE_TYPE_RTC; + this.channelName = channelName; + this.uid = uid; + } + + public String getChannelName() { + return this.channelName; + } + + public String getUid() { + return this.uid; + } + + public ByteBuf pack(ByteBuf buf) { + return super.pack(buf).put(this.channelName).put(this.uid); + } + + public void unpack(ByteBuf byteBuf) { + super.unpack(byteBuf); + this.channelName = byteBuf.readString(); + this.uid = byteBuf.readString(); + } + } + + public static class ServiceRtm extends Service { + public String userId; + + public ServiceRtm() { + this.type = SERVICE_TYPE_RTM; + } + + public ServiceRtm(String userId) { + this.type = SERVICE_TYPE_RTM; + this.userId = userId; + } + + public String getUserId() { + return this.userId; + } + + public ByteBuf pack(ByteBuf buf) { + return super.pack(buf).put(this.userId); + } + + public void unpack(ByteBuf byteBuf) { + super.unpack(byteBuf); + this.userId = byteBuf.readString(); + } + } + + public static class ServiceFpa extends Service { + public ServiceFpa() { + this.type = SERVICE_TYPE_FPA; + } + + public ByteBuf pack(ByteBuf buf) { + return super.pack(buf); + } + + public void unpack(ByteBuf byteBuf) { + super.unpack(byteBuf); + } + } + + public static class ServiceChat extends Service { + public String userId; + + public ServiceChat() { + this.type = SERVICE_TYPE_CHAT; + this.userId = ""; + } + + public ServiceChat(String userId) { + this.type = SERVICE_TYPE_CHAT; + this.userId = userId; + } + + public String getUserId() { + return this.userId; + } + + public ByteBuf pack(ByteBuf buf) { + return super.pack(buf).put(this.userId); + } + + public void unpack(ByteBuf byteBuf) { + super.unpack(byteBuf); + this.userId = byteBuf.readString(); + } + } + + public static class ServiceEducation extends Service { + public String roomUuid; + public String userUuid; + public Short role; + + public ServiceEducation() { + this.type = SERVICE_TYPE_EDUCATION; + this.roomUuid = ""; + this.userUuid = ""; + this.role = -1; + } + + public ServiceEducation(String roomUuid, String userUuid, Short role) { + this.type = SERVICE_TYPE_EDUCATION; + this.roomUuid = roomUuid; + this.userUuid = userUuid; + this.role = role; + } + + public ServiceEducation(String userUuid) { + this.type = SERVICE_TYPE_EDUCATION; + this.roomUuid = ""; + this.userUuid = userUuid; + this.role = -1; + } + + public String getRoomUuid() { + return this.roomUuid; + } + + public String getUserUuid() { + return this.userUuid; + } + + public Short getRole() { + return this.role; + } + + public ByteBuf pack(ByteBuf buf) { + return super.pack(buf).put(this.roomUuid).put(this.userUuid).put(this.role); + } + + public void unpack(ByteBuf byteBuf) { + super.unpack(byteBuf); + this.roomUuid = byteBuf.readString(); + this.userUuid = byteBuf.readString(); + this.role = byteBuf.readShort(); + } + } +} diff --git a/src/main/java/io/agora/media/ByteBuf.java b/src/main/java/io/agora/media/ByteBuf.java new file mode 100644 index 0000000..9a9ef88 --- /dev/null +++ b/src/main/java/io/agora/media/ByteBuf.java @@ -0,0 +1,125 @@ +package io.agora.media; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.TreeMap; + +/** + * Created by Li on 10/1/2016. + */ +public class ByteBuf { + ByteBuffer buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); + + public ByteBuf() { + } + + public ByteBuf(byte[] bytes) { + this.buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + } + + public byte[] asBytes() { + byte[] out = new byte[buffer.position()]; + buffer.rewind(); + buffer.get(out, 0, out.length); + return out; + } + + // packUint16 + public ByteBuf put(short v) { + buffer.putShort(v); + return this; + } + + public ByteBuf put(byte[] v) { + put((short)v.length); + buffer.put(v); + return this; + } + + // packUint32 + public ByteBuf put(int v) { + buffer.putInt(v); + return this; + } + + public ByteBuf put(long v) { + buffer.putLong(v); + return this; + } + + public ByteBuf put(String v) { + return put(v.getBytes()); + } + + public ByteBuf put(TreeMap extra) { + put((short)extra.size()); + + for (Map.Entry pair : extra.entrySet()) { + put(pair.getKey()); + put(pair.getValue()); + } + + return this; + } + + public ByteBuf putIntMap(TreeMap extra) { + put((short)extra.size()); + + for (Map.Entry pair : extra.entrySet()) { + put(pair.getKey()); + put(pair.getValue()); + } + + return this; + } + + public short readShort() { + return buffer.getShort(); + } + + + public int readInt() { + return buffer.getInt(); + } + + public byte[] readBytes() { + short length = readShort(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + public String readString() { + byte[] bytes = readBytes(); + return new String(bytes); + } + + public TreeMap readMap() { + TreeMap map = new TreeMap<>(); + + short length = readShort(); + + for (short i = 0; i < length; ++i) { + short k = readShort(); + String v = readString(); + map.put(k, v); + } + + return map; + } + + public TreeMap readIntMap() { + TreeMap map = new TreeMap<>(); + + short length = readShort(); + + for (short i = 0; i < length; ++i) { + short k = readShort(); + Integer v = readInt(); + map.put(k, v); + } + + return map; + } +} diff --git a/src/main/java/io/agora/media/DynamicKey.java b/src/main/java/io/agora/media/DynamicKey.java new file mode 100644 index 0000000..0f220de --- /dev/null +++ b/src/main/java/io/agora/media/DynamicKey.java @@ -0,0 +1,37 @@ +package io.agora.media; + +import java.io.ByteArrayOutputStream; + +/** + * Created by hefeng on 15/8/10. + * Util to generate Agora media dynamic key. + */ +public class DynamicKey { + /** + * Generate Dynamic Key for media channel service + * @param appID App ID assigned by Agora + * @param appCertificate App Certificate assigned by Agora + * @param channelName name of channel to join, limited to 64 bytes and should be printable ASCII characters + * @param unixTs unix timestamp in seconds when generating the Dynamic Key + * @param randomInt salt for generating dynamic key + * @return String representation of dynamic key + * @throws Exception + */ + public static String generate(String appID, String appCertificate, String channelName, int unixTs, int randomInt) throws Exception { + String unixTsStr = ("0000000000" + Integer.toString(unixTs)).substring(Integer.toString(unixTs).length()); + String randomIntStr = ("00000000" + Integer.toHexString(randomInt)).substring(Integer.toHexString(randomInt).length()); + String signature = generateSignature(appID, appCertificate, channelName, unixTsStr, randomIntStr); + return String.format("%s%s%s%s", signature, appID, unixTsStr, randomIntStr); + } + + private static String generateSignature(String appID, String appCertificate, String channelName, String unixTsStr, String randomIntStr) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(appID.getBytes()); + baos.write(unixTsStr.getBytes()); + baos.write(randomIntStr.getBytes()); + baos.write(channelName.getBytes()); + byte[] sign = DynamicKeyUtil.encodeHMAC(appCertificate, baos.toByteArray()); + return DynamicKeyUtil.bytesToHex(sign); + } + +} diff --git a/src/main/java/io/agora/media/DynamicKey3.java b/src/main/java/io/agora/media/DynamicKey3.java new file mode 100644 index 0000000..ad0dff3 --- /dev/null +++ b/src/main/java/io/agora/media/DynamicKey3.java @@ -0,0 +1,40 @@ +package io.agora.media; + +import java.io.ByteArrayOutputStream; + +public class DynamicKey3 { + + /** + * Manipulate Agora dynamic key for media connection. + * + * @param appID App ID assigned by Agora when register + * @param appCertificate App Certificate assigned by Agora + * @param channelName name of channel to join + * @param unixTs unix timestamp by seconds + * @param randomInt random uint32 salt for generating dynamic key + * @return String representation of dynamic key to join Agora media server + * @throws Exception if any error occurs + */ + public static String generate(String appID, String appCertificate, String channelName, int unixTs, int randomInt, long uid, int expiredTs) throws Exception { + String version = "003"; + String unixTsStr = ("0000000000" + Integer.toString(unixTs)).substring(Integer.toString(unixTs).length()); + String randomIntStr = ("00000000" + Integer.toHexString(randomInt)).substring(Integer.toHexString(randomInt).length()); + uid = uid & 0xFFFFFFFFL; + String uidStr = ("0000000000" + Long.toString(uid)).substring(Long.toString(uid).length()); + String expiredTsStr = ("0000000000" + Integer.toString(expiredTs)).substring(Integer.toString(expiredTs).length()); + String signature = generateSignature3(appID, appCertificate, channelName, unixTsStr, randomIntStr, uidStr, expiredTsStr); + return String.format("%s%s%s%s%s%s%s", version, signature, appID, unixTsStr, randomIntStr, uidStr, expiredTsStr); + } + + private static String generateSignature3(String appID, String appCertificate, String channelName, String unixTsStr, String randomIntStr, String uidStr, String expiredTsStr) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(appID.getBytes()); + baos.write(unixTsStr.getBytes()); + baos.write(randomIntStr.getBytes()); + baos.write(channelName.getBytes()); + baos.write(uidStr.getBytes()); + baos.write(expiredTsStr.getBytes()); + byte[] sign = DynamicKeyUtil.encodeHMAC(appCertificate, baos.toByteArray()); + return DynamicKeyUtil.bytesToHex(sign); + } +} diff --git a/src/main/java/io/agora/media/DynamicKey4.java b/src/main/java/io/agora/media/DynamicKey4.java new file mode 100644 index 0000000..f1e4ef3 --- /dev/null +++ b/src/main/java/io/agora/media/DynamicKey4.java @@ -0,0 +1,82 @@ +package io.agora.media; + +import java.io.ByteArrayOutputStream; + +public class DynamicKey4 { + + private static final String PUBLIC_SHARING_SERVICE = "APSS"; + private static final String RECORDING_SERVICE = "ARS"; + private static final String MEDIA_CHANNEL_SERVICE = "ACS"; + /** + * Generate Dynamic Key for Public Sharing Service + * @param appID App IDassigned by Agora + * @param appCertificate App Certificate assigned by Agora + * @param channelName name of channel to join, limited to 64 bytes and should be printable ASCII characters + * @param unixTs unix timestamp in seconds when generating the Dynamic Key + * @param randomInt salt for generating dynamic key + * @param uid user id, range from 0 - max uint32 + * @param expiredTs should be 0 + * @return String representation of dynamic key + * @throws Exception if any error occurs + */ + public static String generatePublicSharingKey(String appID, String appCertificate, String channelName, int unixTs, int randomInt, long uid, int expiredTs) throws Exception { + return doGenerate(appID, appCertificate, channelName, unixTs, randomInt, uid, expiredTs, PUBLIC_SHARING_SERVICE); + } + + + /** + * Generate Dynamic Key for recording service + * @param appID Vendor key assigned by Agora + * @param appCertificate Sign key assigned by Agora + * @param channelName name of channel to join, limited to 64 bytes and should be printable ASCII characters + * @param unixTs unix timestamp in seconds when generating the Dynamic Key + * @param randomInt salt for generating dynamic key + * @param uid user id, range from 0 - max uint32 + * @param expiredTs should be 0 + * @return String representation of dynamic key + * @throws Exception if any error occurs + */ + public static String generateRecordingKey(String appID, String appCertificate, String channelName, int unixTs, int randomInt, long uid, int expiredTs) throws Exception { + return doGenerate(appID, appCertificate, channelName, unixTs, randomInt, uid, expiredTs, RECORDING_SERVICE); + } + + /** + * Generate Dynamic Key for media channel service + * @param appID Vendor key assigned by Agora + * @param appCertificate Sign key assigned by Agora + * @param channelName name of channel to join, limited to 64 bytes and should be printable ASCII characters + * @param unixTs unix timestamp in seconds when generating the Dynamic Key + * @param randomInt salt for generating dynamic key + * @param uid user id, range from 0 - max uint32 + * @param expiredTs service expiring timestamp. After this timestamp, user will not be able to stay in the channel. + * @return String representation of dynamic key + * @throws Exception if any error occurs + */ + public static String generateMediaChannelKey(String appID, String appCertificate, String channelName, int unixTs, int randomInt, long uid, int expiredTs) throws Exception { + return doGenerate(appID, appCertificate, channelName, unixTs, randomInt, uid, expiredTs, MEDIA_CHANNEL_SERVICE); + } + + private static String doGenerate(String appID, String appCertificate, String channelName, int unixTs, int randomInt, long uid, int expiredTs, String serviceType) throws Exception { + String version = "004"; + String unixTsStr = ("0000000000" + Integer.toString(unixTs)).substring(Integer.toString(unixTs).length()); + String randomIntStr = ("00000000" + Integer.toHexString(randomInt)).substring(Integer.toHexString(randomInt).length()); + uid = uid & 0xFFFFFFFFL; + String uidStr = ("0000000000" + Long.toString(uid)).substring(Long.toString(uid).length()); + String expiredTsStr = ("0000000000" + Integer.toString(expiredTs)).substring(Integer.toString(expiredTs).length()); + String signature = generateSignature4(appID, appCertificate, channelName, unixTsStr, randomIntStr, uidStr, expiredTsStr, serviceType); + return String.format("%s%s%s%s%s%s", version, signature, appID, unixTsStr, randomIntStr, expiredTsStr); + } + + private static String generateSignature4(String appID, String appCertificate, String channelName, String unixTsStr, String randomIntStr, String uidStr, String expiredTsStr, String serviceType) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(serviceType.getBytes()); + baos.write(appID.getBytes()); + baos.write(unixTsStr.getBytes()); + baos.write(randomIntStr.getBytes()); + baos.write(channelName.getBytes()); + baos.write(uidStr.getBytes()); + baos.write(expiredTsStr.getBytes()); + byte[] sign = DynamicKeyUtil.encodeHMAC(appCertificate, baos.toByteArray()); + return DynamicKeyUtil.bytesToHex(sign); + } +} diff --git a/src/main/java/io/agora/media/DynamicKey5.java b/src/main/java/io/agora/media/DynamicKey5.java new file mode 100644 index 0000000..914a851 --- /dev/null +++ b/src/main/java/io/agora/media/DynamicKey5.java @@ -0,0 +1,149 @@ +package io.agora.media; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +import java.util.TreeMap; + +/** + * Created by Li on 10/1/2016. + */ +public class DynamicKey5 { + public final static String version = "005"; + public final static String noUpload = "0"; + public final static String audioVideoUpload = "3"; + + // ServiceType + public final static short MEDIA_CHANNEL_SERVICE = 1; + public final static short RECORDING_SERVICE = 2; + public final static short PUBLIC_SHARING_SERVICE = 3; + public final static short IN_CHANNEL_PERMISSION = 4; + + // InChannelPermissionKey + public final static short ALLOW_UPLOAD_IN_CHANNEL = 1; + + public DynamicKey5Content content; + + public boolean fromString(String key) { + if (! key.substring(0, 3).equals(version)) { + return false; + } + + byte[] rawContent = new Base64().decode(key.substring(3)); + if (rawContent.length == 0) { + return false; + } + + content = new DynamicKey5Content(); + ByteBuf buffer = new ByteBuf(rawContent); + content.unmarshall(buffer); + return true; + } + + public static String generateSignature(String appCertificate, short service, String appID, int unixTs, int salt, String channelName, long uid, int expiredTs, TreeMap extra) throws Exception { + // decode hex to avoid case problem + Hex hex = new Hex(); + byte[] rawAppID = hex.decode(appID.getBytes()); + byte[] rawAppCertificate = hex.decode(appCertificate.getBytes()); + + Message m = new Message(service, rawAppID, unixTs, salt, channelName, (int)(uid & 0xFFFFFFFFL), expiredTs, extra); + byte[] toSign = pack(m); + return new String(Hex.encodeHex(DynamicKeyUtil.encodeHMAC(rawAppCertificate, toSign), false)); + } + + public static String generateDynamicKey(String appID, String appCertificate, String channel, int ts, int salt, long uid, int expiredTs, TreeMap extra, short service) throws Exception { + String signature = generateSignature(appCertificate, service, appID, ts, salt, channel, uid, expiredTs, extra); + DynamicKey5Content content = new DynamicKey5Content(service, signature, new Hex().decode(appID.getBytes()), ts, salt, expiredTs, extra); + byte[] bytes = pack(content); + byte[] encoded = new Base64().encode(bytes); + String base64 = new String(encoded); + return version + base64; + } + + private static byte[] pack(Packable content) { + ByteBuf buffer = new ByteBuf(); + content.marshal(buffer); + return buffer.asBytes(); + } + + public static String generatePublicSharingKey(String appID, String appCertificate, String channel, int ts, int salt, long uid, int expiredTs) throws Exception { + return generateDynamicKey(appID, appCertificate, channel, ts, salt, uid, expiredTs, new TreeMap(), PUBLIC_SHARING_SERVICE); + } + + public static String generateRecordingKey(String appID, String appCertificate, String channel, int ts, int salt, long uid, int expiredTs) throws Exception { + return generateDynamicKey(appID, appCertificate, channel, ts, salt, uid, expiredTs, new TreeMap(), RECORDING_SERVICE); + } + + public static String generateMediaChannelKey(String appID, String appCertificate, String channel, int ts, int salt, long uid, int expiredTs) throws Exception { + return generateDynamicKey(appID, appCertificate, channel, ts, salt, uid, expiredTs, new TreeMap(), MEDIA_CHANNEL_SERVICE); + } + + public static String generateInChannelPermissionKey(String appID, String appCertificate, String channel, int ts, int salt, long uid, int expiredTs, String permission) throws Exception { + TreeMap extra = new TreeMap(); + extra.put(ALLOW_UPLOAD_IN_CHANNEL, permission); + return generateDynamicKey(appID, appCertificate, channel, ts, salt, uid, expiredTs, extra, IN_CHANNEL_PERMISSION); + } + + static class Message implements Packable { + public short serviceType; + public byte[] appID; + public int unixTs; + public int salt; + public String channelName; + public int uid; + public int expiredTs; + public TreeMap extra; + + public Message(short serviceType, byte[] appID, int unixTs, int salt, String channelName, int uid, int expiredTs, TreeMap extra) { + this.serviceType = serviceType; + this.appID = appID; + this.unixTs = unixTs; + this.salt = salt; + this.channelName = channelName; + this.uid = uid; + this.expiredTs = expiredTs; + this.extra = extra; + } + + public ByteBuf marshal(ByteBuf out) { + return out.put(serviceType).put(appID).put(unixTs).put(salt).put(channelName).put(uid).put(expiredTs).put(extra); + } + } + + public static class DynamicKey5Content implements Packable { + public short serviceType; + public String signature; + public byte[] appID; + public int unixTs; + public int salt; + public int expiredTs; + public TreeMap extra; + + public DynamicKey5Content() { + } + + public DynamicKey5Content(short serviceType, String signature, byte[] appID, int unixTs, int salt, int expiredTs, TreeMap extra) { + this.serviceType = serviceType; + this.signature = signature; + this.appID = appID; + this.unixTs = unixTs; + this.salt = salt; + this.expiredTs = expiredTs; + this.extra = extra; + } + + public ByteBuf marshal(ByteBuf out) { + return out.put(serviceType).put(signature).put(appID).put(unixTs).put(salt).put(expiredTs).put(extra); + } + + public void unmarshall(ByteBuf in) { + this.serviceType = in.readShort(); + this.signature = in.readString(); + this.appID = in.readBytes(); + this.unixTs = in.readInt(); + this.salt = in.readInt(); + this.expiredTs = in.readInt(); + this.extra = in.readMap(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/agora/media/DynamicKeyUtil.java b/src/main/java/io/agora/media/DynamicKeyUtil.java new file mode 100644 index 0000000..af4bfc5 --- /dev/null +++ b/src/main/java/io/agora/media/DynamicKeyUtil.java @@ -0,0 +1,33 @@ +package io.agora.media; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * Created by hefeng on 15/8/10. + * Util to generate Agora media dynamic key. + */ +public class DynamicKeyUtil { + + static byte[] encodeHMAC(String key, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException { + return encodeHMAC(key.getBytes(), message); + } + + static byte[] encodeHMAC(byte[] key, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException { + SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA1"); + + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(keySpec); + return mac.doFinal(message); + } + + static String bytesToHex(byte[] in) { + final StringBuilder builder = new StringBuilder(); + for (byte b : in) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } +} diff --git a/src/main/java/io/agora/media/FpaTokenBuilder.java b/src/main/java/io/agora/media/FpaTokenBuilder.java new file mode 100644 index 0000000..443c922 --- /dev/null +++ b/src/main/java/io/agora/media/FpaTokenBuilder.java @@ -0,0 +1,27 @@ +package io.agora.media; + +public class FpaTokenBuilder { + /** + * Build the FPA token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @return The FPA token. + */ + public String buildToken(String appId, String appCertificate) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, 24 * 3600); + AccessToken2.Service serviceFpa = new AccessToken2.ServiceFpa(); + + serviceFpa.addPrivilegeFpa(AccessToken2.PrivilegeFpa.PRIVILEGE_LOGIN, 0); + accessToken.addService(serviceFpa); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/media/Packable.java b/src/main/java/io/agora/media/Packable.java new file mode 100644 index 0000000..916c36c --- /dev/null +++ b/src/main/java/io/agora/media/Packable.java @@ -0,0 +1,8 @@ +package io.agora.media; + +/** + * Created by Li on 10/1/2016. + */ +public interface Packable { + ByteBuf marshal(ByteBuf out); +} diff --git a/src/main/java/io/agora/media/PackableEx.java b/src/main/java/io/agora/media/PackableEx.java new file mode 100644 index 0000000..76e274b --- /dev/null +++ b/src/main/java/io/agora/media/PackableEx.java @@ -0,0 +1,5 @@ +package io.agora.media; + +public interface PackableEx extends Packable { + void unmarshal(ByteBuf in); +} diff --git a/src/main/java/io/agora/media/RtcTokenBuilder.java b/src/main/java/io/agora/media/RtcTokenBuilder.java new file mode 100644 index 0000000..2c85246 --- /dev/null +++ b/src/main/java/io/agora/media/RtcTokenBuilder.java @@ -0,0 +1,108 @@ +package io.agora.media; + +public class RtcTokenBuilder { + public enum Role { + /** + * DEPRECATED. Role_Attendee has the same privileges as Role_Publisher. + */ + Role_Attendee(0), + /** + * RECOMMENDED. Use this role for a voice/video call or a live broadcast, if your scenario does not require authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). + */ + Role_Publisher(1), + /** + * Only use this role if your scenario require authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). + * + * @note In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher. + */ + Role_Subscriber(2), + /** + * DEPRECATED. Role_Attendee has the same privileges as Role_Publisher. + */ + Role_Admin(101); + + public int initValue; + + Role(int initValue) { + this.initValue = initValue; + } + } + + /** + * Builds an RTC token using an int uid. + * + * @param appId The App ID issued to you by Agora. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. + * @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + *
    + *
  • The 26 lowercase English letters: a to z.
  • + *
  • The 26 uppercase English letters: A to Z.
  • + *
  • The 10 digits: 0 to 9.
  • + *
  • The space.
  • + *
  • "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + *
+ * @param uid User ID. A 32-bit unsigned integer with a value ranging from + * 1 to (2^32-1). + * @param role The user role. + *
    + *
  • Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
  • + *
  • Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
  • + *
+ * @param privilegeTs Represented by the number of seconds elapsed since 1/1/1970. + * If, for example, you want to access the Agora Service within 10 minutes + * after the token is generated, set expireTimestamp as the current time stamp + * + 600 (seconds). + */ + public String buildTokenWithUid(String appId, String appCertificate, + String channelName, int uid, Role role, int privilegeTs) { + String account = uid == 0 ? "" : String.valueOf(uid); + return buildTokenWithUserAccount(appId, appCertificate, channelName, + account, role, privilegeTs); + } + + /** + * Builds an RTC token using a string userAccount. + * + * @param appId The App ID issued to you by Agora. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. + * @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + *
    + *
  • The 26 lowercase English letters: a to z.
  • + *
  • The 26 uppercase English letters: A to Z.
  • + *
  • The 10 digits: 0 to 9.
  • + *
  • The space.
  • + *
  • "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + *
+ * @param account The user account. + * @param role The user role. + *
    + *
  • Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
  • + *
  • Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
  • + *
+ * @param privilegeTs represented by the number of seconds elapsed since 1/1/1970. + * If, for example, you want to access the Agora Service within 10 minutes + * after the token is generated, set expireTimestamp as the current time stamp + * + 600 (seconds). + */ + public String buildTokenWithUserAccount(String appId, String appCertificate, + String channelName, String account, Role role, int privilegeTs) { + + // Assign appropriate access privileges to each role. + AccessToken builder = new AccessToken(appId, appCertificate, channelName, account); + builder.addPrivilege(AccessToken.Privileges.kJoinChannel, privilegeTs); + if (role == Role.Role_Publisher || role == Role.Role_Subscriber || role == Role.Role_Admin) { + builder.addPrivilege(AccessToken.Privileges.kPublishAudioStream, privilegeTs); + builder.addPrivilege(AccessToken.Privileges.kPublishVideoStream, privilegeTs); + builder.addPrivilege(AccessToken.Privileges.kPublishDataStream, privilegeTs); + } + + try { + return builder.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/media/RtcTokenBuilder2.java b/src/main/java/io/agora/media/RtcTokenBuilder2.java new file mode 100644 index 0000000..eb6da6c --- /dev/null +++ b/src/main/java/io/agora/media/RtcTokenBuilder2.java @@ -0,0 +1,203 @@ +package io.agora.media; + +public class RtcTokenBuilder2 { + public enum Role { + ROLE_PUBLISHER(1), + ROLE_SUBSCRIBER(2), + ; + + public int initValue; + + Role(int initValue) { + this.initValue = initValue; + } + } + + /** + * Build the RTC token with uid. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param uid: User ID. A 32-bit unsigned integer with a value ranging from 1 to (232-1). + * optionalUid must be unique. + * @param role: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param token_expire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set token_expire as 600(seconds). + * @param privilege_expire: represented by the number of seconds elapsed since now. If, for example, + * you want to enable your privilege for 10 minutes, set privilege_expire as 600(seconds). + * @return The RTC token. + */ + public String buildTokenWithUid(String appId, String appCertificate, String channelName, int uid, Role role, int token_expire, int privilege_expire) { + return buildTokenWithUserAccount(appId, appCertificate, channelName, AccessToken2.getUidStr(uid), role, token_expire, privilege_expire); + } + + /** + * Build the RTC token with account. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param account: The user's account, max length is 255 Bytes. + * @param role: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param token_expire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set token_expire as 600(seconds). + * @param privilege_expire: represented by the number of seconds elapsed since now. If, for example, + * you want to enable your privilege for 10 minutes, set privilege_expire as 600(seconds). + * @return The RTC token. + */ + public String buildTokenWithUserAccount(String appId, String appCertificate, String channelName, String account, Role role, int token_expire, int privilege_expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, token_expire); + AccessToken2.Service serviceRtc = new AccessToken2.ServiceRtc(channelName, account); + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, privilege_expire); + if (role == Role.ROLE_PUBLISHER) { + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, privilege_expire); + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, privilege_expire); + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, privilege_expire); + } + accessToken.addService(serviceRtc); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + /** + * Generates an RTC token with the specified privilege. + *

+ * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + *

+ * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + *

+ * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. The channel name may contain the following characters: + * - All lowercase English letters: a to z. + * - All uppercase English letters: A to Z. + * - All numeric characters: 0 to 9. + * - The space character. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param uid The user ID. A 32-bit unsigned integer with a value range from 1 to (232 - 1). It must be unique. Set uid as 0, if you do not want to authenticate the user ID, that is, any uid from the app client can join the channel. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set tokenExpire as 600(seconds). + * @param joinChannelPrivilegeExpire The Unix timestamp when the privilege for joining the channel expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set joinChannelPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. + * @param pubAudioPrivilegeExpire The Unix timestamp when the privilege for publishing audio expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubAudioPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubAudioPrivilegeExpire as the current Unix timestamp. + * @param pubVideoPrivilegeExpire The Unix timestamp when the privilege for publishing video expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubVideoPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubVideoPrivilegeExpire as the current Unix timestamp. + * @param pubDataStreamPrivilegeExpire The Unix timestamp when the privilege for publishing data streams expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubDataStreamPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubDataStreamPrivilegeExpire as the current Unix timestamp. + * @note Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + */ + public String buildTokenWithUid(String appId, String appCertificate, String channelName, int uid, + int tokenExpire, int joinChannelPrivilegeExpire, int pubAudioPrivilegeExpire, + int pubVideoPrivilegeExpire, int pubDataStreamPrivilegeExpire) { + return buildTokenWithUserAccount(appId, appCertificate, channelName, AccessToken2.getUidStr(uid), + tokenExpire, joinChannelPrivilegeExpire, pubAudioPrivilegeExpire, pubVideoPrivilegeExpire, pubDataStreamPrivilegeExpire); + } + + /** + * Generates an RTC token with the specified privilege. + *

+ * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + *

+ * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + *

+ * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. The channel name may contain the following characters: + * - All lowercase English letters: a to z. + * - All uppercase English letters: A to Z. + * - All numeric characters: 0 to 9. + * - The space character. + * - "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * @param account The user account. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set tokenExpire as 600(seconds). + * @param joinChannelPrivilegeExpire The Unix timestamp when the privilege for joining the channel expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set joinChannelPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. + * @param pubAudioPrivilegeExpire The Unix timestamp when the privilege for publishing audio expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubAudioPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubAudioPrivilegeExpire as the current Unix timestamp. + * @param pubVideoPrivilegeExpire The Unix timestamp when the privilege for publishing video expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubVideoPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubVideoPrivilegeExpire as the current Unix timestamp. + * @param pubDataStreamPrivilegeExpire The Unix timestamp when the privilege for publishing data streams expires, represented + * by the sum of the current timestamp plus the valid time period of the token. For example, if you set pubDataStreamPrivilegeExpire as the + * current timestamp plus 600 seconds, the token expires in 10 minutes. If you do not want to enable this privilege, + * set pubDataStreamPrivilegeExpire as the current Unix timestamp. + * @note Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + */ + public String buildTokenWithUserAccount(String appId, String appCertificate, String channelName, String account, + int tokenExpire, int joinChannelPrivilegeExpire, int pubAudioPrivilegeExpire, + int pubVideoPrivilegeExpire, int pubDataStreamPrivilegeExpire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, tokenExpire); + AccessToken2.Service serviceRtc = new AccessToken2.ServiceRtc(channelName, account); + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, joinChannelPrivilegeExpire); + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, pubAudioPrivilegeExpire); + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, pubVideoPrivilegeExpire); + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, pubDataStreamPrivilegeExpire); + accessToken.addService(serviceRtc); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/media/Utils.java b/src/main/java/io/agora/media/Utils.java new file mode 100644 index 0000000..537b0e5 --- /dev/null +++ b/src/main/java/io/agora/media/Utils.java @@ -0,0 +1,138 @@ +package io.agora.media; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Date; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public class Utils { + public static final long HMAC_SHA256_LENGTH = 32; + public static final int VERSION_LENGTH = 3; + public static final int APP_ID_LENGTH = 32; + + public static byte[] hmacSign(String keyString, byte[] msg) throws InvalidKeyException, NoSuchAlgorithmException { + SecretKeySpec keySpec = new SecretKeySpec(keyString.getBytes(), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(keySpec); + return mac.doFinal(msg); + } + + public static byte[] pack(PackableEx packableEx) { + ByteBuf buffer = new ByteBuf(); + packableEx.marshal(buffer); + return buffer.asBytes(); + } + + public static void unpack(byte[] data, PackableEx packableEx) { + ByteBuf buffer = new ByteBuf(data); + packableEx.unmarshal(buffer); + } + + public static String base64Encode(byte[] data) { + byte[] encodedBytes = Base64.encodeBase64(data); + return new String(encodedBytes); + } + + public static byte[] base64Decode(String data) { + return Base64.decodeBase64(data.getBytes()); + } + + public static int crc32(String data) { + // get bytes from string + byte[] bytes = data.getBytes(); + return crc32(bytes); + } + + public static int crc32(byte[] bytes) { + CRC32 checksum = new CRC32(); + checksum.update(bytes); + return (int)checksum.getValue(); + } + + public static int getTimestamp() { + return (int)((new Date().getTime())/1000); + } + + public static int randomInt() { + return new SecureRandom().nextInt(); + } + + public static boolean isUUID(String uuid) { + if (uuid.length() != 32) { + return false; + } + + return uuid.matches("\\p{XDigit}+"); + } + + public static byte[] compress(byte[] data) { + byte[] output; + Deflater deflater = new Deflater(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); + + try { + deflater.reset(); + deflater.setInput(data); + deflater.finish(); + + byte[] buf = new byte[data.length]; + while (!deflater.finished()) { + int i = deflater.deflate(buf); + bos.write(buf, 0, i); + } + output = bos.toByteArray(); + } catch (Exception e) { + output = data; + e.printStackTrace(); + } finally { + deflater.end(); + } + + return output; + } + + public static byte[] decompress(byte[] data) { + Inflater inflater = new Inflater(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); + + try { + inflater.setInput(data); + byte[] buf = new byte[data.length]; + while (!inflater.finished()) { + int i = inflater.inflate(buf); + bos.write(buf, 0, i); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + inflater.end(); + } + + return bos.toByteArray(); + } + + public static String md5(String plainText) { + byte[] secretBytes = null; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("No md5 digest!"); + } + String md5code = new BigInteger(1, secretBytes).toString(16); + for (int i = 0; i < 32 - md5code.length(); i++) { + md5code = "0" + md5code; + } + return md5code; + } +} diff --git a/src/main/java/io/agora/rtm/RtmTokenBuilder.java b/src/main/java/io/agora/rtm/RtmTokenBuilder.java new file mode 100644 index 0000000..5c2d286 --- /dev/null +++ b/src/main/java/io/agora/rtm/RtmTokenBuilder.java @@ -0,0 +1,32 @@ +package io.agora.rtm; + +import io.agora.media.AccessToken; + +public class RtmTokenBuilder { + public enum Role { + Rtm_User(1); + + int value; + Role(int value) { + this.value = value; + } + } + + public AccessToken mTokenCreator; + + public String buildToken(String appId, String appCertificate, + String uid, Role role, int privilegeTs) throws Exception { + mTokenCreator = new AccessToken(appId, appCertificate, uid, ""); + mTokenCreator.addPrivilege(AccessToken.Privileges.kRtmLogin, privilegeTs); + return mTokenCreator.build(); + } + + public void setPrivilege(AccessToken.Privileges privilege, int expireTs) { + mTokenCreator.addPrivilege(privilege, expireTs); + } + + public boolean initTokenBuilder(String originToken) { + mTokenCreator.fromString(originToken); + return true; + } +} diff --git a/src/main/java/io/agora/rtm/RtmTokenBuilder2.java b/src/main/java/io/agora/rtm/RtmTokenBuilder2.java new file mode 100644 index 0000000..dd7cba9 --- /dev/null +++ b/src/main/java/io/agora/rtm/RtmTokenBuilder2.java @@ -0,0 +1,33 @@ +package io.agora.rtm; + +import io.agora.media.AccessToken2; + +public class RtmTokenBuilder2 { + + /** + * Build the RTM token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param userId: The user's account, max length is 64 Bytes. + * @param expire: represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expireTimestamp as 600(seconds). + * @return The RTM token. + */ + public String buildToken(String appId, String appCertificate, String userId, int expire) { + AccessToken2 accessToken = new AccessToken2(appId, appCertificate, expire); + AccessToken2.Service serviceRtm = new AccessToken2.ServiceRtm(userId); + + serviceRtm.addPrivilegeRtm(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN, expire); + accessToken.addService(serviceRtm); + + try { + return accessToken.build(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } +} diff --git a/src/main/java/io/agora/sample/AccessTokenInspector.java b/src/main/java/io/agora/sample/AccessTokenInspector.java new file mode 100644 index 0000000..35625df --- /dev/null +++ b/src/main/java/io/agora/sample/AccessTokenInspector.java @@ -0,0 +1,92 @@ +package io.agora.sample; + +import io.agora.media.AccessToken2; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +public class AccessTokenInspector { + public static void main(String[] args) { + AccessTokenInspector inspector = new AccessTokenInspector(); + inspector.inspect(args[0]); + } + + void inspect(String input) { + AccessToken2 token = new AccessToken2(); + System.out.printf("parsing token: %s\n\n", input); + token.parse(input); + System.out.printf("appId:%s\n", token.appId); + System.out.printf("appCert:%s\n", token.appCert); + System.out.printf("salt:%d\n", token.salt); + System.out.printf("issueTs:%d\n", token.issueTs); + System.out.printf("expire:%d\n", token.expire); + System.out.printf("services:\n"); + + for (AccessToken2.Service service : token.services.values()) { + System.out.printf("\t{%s}\n", toServiceStr(service)); + } + } + + String toServiceStr(AccessToken2.Service service) { + if (service.getServiceType() == AccessToken2.SERVICE_TYPE_RTC) { + AccessToken2.ServiceRtc serviceRtc = (AccessToken2.ServiceRtc) service; + return String.format("type:rtc, channel:%s, uid: %s, privileges: [%s]}", serviceRtc.getChannelName(), + serviceRtc.getUid(), toRtcPrivileges(serviceRtc.getPrivileges())); + } else if (service.getServiceType() == AccessToken2.SERVICE_TYPE_RTM) { + AccessToken2.ServiceRtm serviceRtm = (AccessToken2.ServiceRtm) service; + return String.format("type:rtm, user_id:%s, privileges:[%s]", serviceRtm.getUserId(), + toRtmPrivileges(serviceRtm.getPrivileges())); + } else if (service.getServiceType() == AccessToken2.SERVICE_TYPE_CHAT) { + AccessToken2.ServiceChat serviceChat = (AccessToken2.ServiceChat) service; + return String.format("type:chat, user_id:%s, privileges:[%s]", serviceChat.getUserId(), + toChatPrivileges(serviceChat.getPrivileges())); + } + return "unknown"; + } + + + + private String toRtcPrivileges(TreeMap privileges) { + List privilegeStrList = new ArrayList<>(privileges.size()); + if (privileges.containsKey(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL.intValue)) { + privilegeStrList.add(String.format("JOIN_CHANNEL(%d)", + privileges.get(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL.intValue))); + } + if (privileges.containsKey(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM.intValue)) { + privilegeStrList.add(String.format("PUBLISH_AUDIO_STREAM(%d)", + privileges.get(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM.intValue))); + } + if (privileges.containsKey(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM.intValue)) { + privilegeStrList.add(String.format("PUBLISH_VIDEO_STREAM(%d)", + privileges.get(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM.intValue))); + } + if (privileges.containsKey(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM.intValue)) { + privilegeStrList.add(String.format("PUBLISH_DATA_STREAM(%d)", + privileges.get(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM.intValue))); + } + return String.join(",", privilegeStrList); + } + + private String toRtmPrivileges(TreeMap privileges) { + List privilegeStrList = new ArrayList<>(privileges.size()); + if (privileges.containsKey(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN.intValue)) { + privilegeStrList.add(String.format("JOIN_LOGIN(%d)", + privileges.get(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN.intValue))); + } + return String.join(",", privilegeStrList); + } + + private String toChatPrivileges(TreeMap privileges) { + List privilegeStrList = new ArrayList<>(privileges.size()); + if (privileges.containsKey(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_USER.intValue)) { + privilegeStrList.add(String.format("USER(%d)", + privileges.get(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_USER.intValue))); + } + if (privileges.containsKey(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_APP.intValue)) { + privilegeStrList.add(String.format("APP(%d)", + privileges.get(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_APP.intValue))); + } + return String.join(",", privilegeStrList); + } +} diff --git a/src/main/java/io/agora/sample/ChatTokenBuilder2Sample.java b/src/main/java/io/agora/sample/ChatTokenBuilder2Sample.java new file mode 100644 index 0000000..a488911 --- /dev/null +++ b/src/main/java/io/agora/sample/ChatTokenBuilder2Sample.java @@ -0,0 +1,18 @@ +package io.agora.sample; + +import io.agora.chat.ChatTokenBuilder2; + +public class ChatTokenBuilder2Sample { + private static String appId = "970CA35de60c44645bbae8a215061b33"; + private static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + private static String userId = "2882341273"; + private static int expire = 600; + + public static void main(String[] args) throws Exception { + ChatTokenBuilder2 tokenBuilder = new ChatTokenBuilder2(); + String userToken = tokenBuilder.buildUserToken(appId, appCertificate, userId, expire); + System.out.printf("Chat user token: %s\n", userToken); + String appToken = tokenBuilder.buildAppToken(appId, appCertificate, expire); + System.out.printf("Chat app token: %s\n", appToken); + } +} diff --git a/src/main/java/io/agora/sample/DynamicKey5Sample.java b/src/main/java/io/agora/sample/DynamicKey5Sample.java new file mode 100644 index 0000000..90f767e --- /dev/null +++ b/src/main/java/io/agora/sample/DynamicKey5Sample.java @@ -0,0 +1,26 @@ +package io.agora.sample; + +import io.agora.media.DynamicKey5; + +import java.util.Date; +import java.util.Random; + +/** + * Created by Li on 10/1/2016. + */ +public class DynamicKey5Sample { + static String appID = "970ca35de60c44645bbae8a215061b33"; + static String appCertificate = "5cfd2fd1755d40ecb72977518be15d3b"; + static String channel = "7d72365eb983485397e3e3f9d460bdda"; + static int ts = (int)(new Date().getTime()/1000); + static int r = new Random().nextInt(); + static long uid = 2882341273L; + static int expiredTs = 0; + + public static void main(String[] args) throws Exception { + System.out.println(DynamicKey5.generateMediaChannelKey(appID, appCertificate, channel, ts, r, uid, expiredTs)); + System.out.println(DynamicKey5.generateRecordingKey(appID, appCertificate, channel, ts, r, uid, expiredTs)); + System.out.println(DynamicKey5.generateInChannelPermissionKey(appID, appCertificate, channel, ts, r, uid, expiredTs, DynamicKey5.noUpload)); + System.out.println(DynamicKey5.generateInChannelPermissionKey(appID, appCertificate, channel, ts, r, uid, expiredTs, DynamicKey5.audioVideoUpload)); + } +} diff --git a/src/main/java/io/agora/sample/EducationTokenBuilder2Sample.java b/src/main/java/io/agora/sample/EducationTokenBuilder2Sample.java new file mode 100644 index 0000000..d0c02d0 --- /dev/null +++ b/src/main/java/io/agora/sample/EducationTokenBuilder2Sample.java @@ -0,0 +1,24 @@ +package io.agora.sample; + +import io.agora.education.EducationTokenBuilder2; + +public class EducationTokenBuilder2Sample { + private static String appId = "970CA35de60c44645bbae8a215061b33"; + private static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + private static String roomUuid = "123"; + private static String userUuid = "2882341273"; + private static Short role = 1; + private static int expire = 600; + + public static void main(String[] args) { + EducationTokenBuilder2 tokenBuilder = new EducationTokenBuilder2(); + String roomUserToken = tokenBuilder.buildRoomUserToken(appId, appCertificate, roomUuid, userUuid, role, expire); + System.out.printf("Education room user token: %s\n", roomUserToken); + + String userToken = tokenBuilder.buildUserToken(appId, appCertificate, userUuid, expire); + System.out.printf("Education user token: %s\n", userToken); + + String appToken = tokenBuilder.buildAppToken(appId, appCertificate, expire); + System.out.printf("Education app token: %s\n", appToken); + } +} diff --git a/src/main/java/io/agora/sample/FpaTokenBuilderSample.java b/src/main/java/io/agora/sample/FpaTokenBuilderSample.java new file mode 100644 index 0000000..24973fb --- /dev/null +++ b/src/main/java/io/agora/sample/FpaTokenBuilderSample.java @@ -0,0 +1,14 @@ +package io.agora.sample; + +import io.agora.media.FpaTokenBuilder; + +public class FpaTokenBuilderSample { + static String appId = "970CA35de60c44645bbae8a215061b33"; + static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + + public static void main(String[] args) { + FpaTokenBuilder token = new FpaTokenBuilder(); + String result = token.buildToken(appId, appCertificate); + System.out.println("Token with FPA service: " + result); + } +} diff --git a/src/main/java/io/agora/sample/RtcTokenBuilder2Sample.java b/src/main/java/io/agora/sample/RtcTokenBuilder2Sample.java new file mode 100644 index 0000000..93f7821 --- /dev/null +++ b/src/main/java/io/agora/sample/RtcTokenBuilder2Sample.java @@ -0,0 +1,29 @@ +package io.agora.sample; + +import io.agora.media.RtcTokenBuilder2; +import io.agora.media.RtcTokenBuilder2.Role; + +public class RtcTokenBuilder2Sample { + static String appId = "970CA35de60c44645bbae8a215061b33"; + static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + static String channelName = "7d72365eb983485397e3e3f9d460bdda"; + static String account = "2082341273"; + static int uid = 2082341273; + static int tokenExpirationInSeconds = 3600; + static int privilegeExpirationInSeconds = 3600; + + public static void main(String[] args) { + RtcTokenBuilder2 token = new RtcTokenBuilder2(); + String result = token.buildTokenWithUid(appId, appCertificate, channelName, uid, Role.ROLE_SUBSCRIBER, tokenExpirationInSeconds, privilegeExpirationInSeconds); + System.out.println("Token with uid: " + result); + + result = token.buildTokenWithUserAccount(appId, appCertificate, channelName, account, Role.ROLE_SUBSCRIBER, tokenExpirationInSeconds, privilegeExpirationInSeconds); + System.out.println("Token with account: " + result); + + result = token.buildTokenWithUid(appId, appCertificate, channelName, uid, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds); + System.out.println("Token with uid and privilege: " + result); + + result = token.buildTokenWithUserAccount(appId, appCertificate, channelName, account, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds, privilegeExpirationInSeconds); + System.out.println("Token with account and privilege: " + result); + } +} diff --git a/src/main/java/io/agora/sample/RtcTokenBuilderSample.java b/src/main/java/io/agora/sample/RtcTokenBuilderSample.java new file mode 100644 index 0000000..8ccb9b2 --- /dev/null +++ b/src/main/java/io/agora/sample/RtcTokenBuilderSample.java @@ -0,0 +1,25 @@ +package io.agora.sample; + +import io.agora.media.RtcTokenBuilder; +import io.agora.media.RtcTokenBuilder.Role; + +public class RtcTokenBuilderSample { + static String appId = "970CA35de60c44645bbae8a215061b33"; + static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + static String channelName = "7d72365eb983485397e3e3f9d460bdda"; + static String userAccount = "2082341273"; + static int uid = 2082341273; + static int expirationTimeInSeconds = 3600; + + public static void main(String[] args) throws Exception { + RtcTokenBuilder token = new RtcTokenBuilder(); + int timestamp = (int)(System.currentTimeMillis() / 1000 + expirationTimeInSeconds); + String result = token.buildTokenWithUserAccount(appId, appCertificate, + channelName, userAccount, Role.Role_Publisher, timestamp); + System.out.println(result); + + result = token.buildTokenWithUid(appId, appCertificate, + channelName, uid, Role.Role_Publisher, timestamp); + System.out.println(result); + } +} diff --git a/src/main/java/io/agora/sample/RtmTokenBuilder2Sample.java b/src/main/java/io/agora/sample/RtmTokenBuilder2Sample.java new file mode 100644 index 0000000..b50c236 --- /dev/null +++ b/src/main/java/io/agora/sample/RtmTokenBuilder2Sample.java @@ -0,0 +1,16 @@ +package io.agora.sample; + +import io.agora.rtm.RtmTokenBuilder2; + +public class RtmTokenBuilder2Sample { + private static String appId = "970CA35de60c44645bbae8a215061b33"; + private static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + private static String userId = "test_user_id"; + private static int expirationInSeconds = 3600; + + public static void main(String[] args) { + RtmTokenBuilder2 token = new RtmTokenBuilder2(); + String result = token.buildToken(appId, appCertificate, userId, expirationInSeconds); + System.out.println("Rtm Token: " + result); + } +} diff --git a/src/main/java/io/agora/sample/RtmTokenBuilderSample.java b/src/main/java/io/agora/sample/RtmTokenBuilderSample.java new file mode 100644 index 0000000..01be794 --- /dev/null +++ b/src/main/java/io/agora/sample/RtmTokenBuilderSample.java @@ -0,0 +1,17 @@ +package io.agora.sample; + +import io.agora.rtm.RtmTokenBuilder; +import io.agora.rtm.RtmTokenBuilder.Role; + +public class RtmTokenBuilderSample { + private static String appId = "970CA35de60c44645bbae8a215061b33"; + private static String appCertificate = "5CFd2fd1755d40ecb72977518be15d3b"; + private static String userId = "2882341273"; + private static int expireTimestamp = 0; + + public static void main(String[] args) throws Exception { + RtmTokenBuilder token = new RtmTokenBuilder(); + String result = token.buildToken(appId, appCertificate, userId, Role.Rtm_User, expireTimestamp); + System.out.println(result); + } +} diff --git a/src/main/java/io/agora/sample/SignalingTokenSample.java b/src/main/java/io/agora/sample/SignalingTokenSample.java new file mode 100644 index 0000000..5f89cd6 --- /dev/null +++ b/src/main/java/io/agora/sample/SignalingTokenSample.java @@ -0,0 +1,21 @@ +package io.agora.sample; + +import io.agora.signal.SignalingToken; + +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +public class SignalingTokenSample { + + public static void main(String []args) throws NoSuchAlgorithmException{ + + String appId = "970ca35de60c44645bbae8a215061b33"; + String certificate = "5cfd2fd1755d40ecb72977518be15d3b"; + String account = "TestAccount"; + //Use the current time plus an available time to guarantee the only time it is obtained + int expiredTsInSeconds = 1446455471 + (int) (new Date().getTime()/1000l); + String result = SignalingToken.getToken(appId, certificate, account, expiredTsInSeconds); + System.out.println(result); + + } +} diff --git a/src/main/java/io/agora/sample/Verifier5.java b/src/main/java/io/agora/sample/Verifier5.java new file mode 100644 index 0000000..8135049 --- /dev/null +++ b/src/main/java/io/agora/sample/Verifier5.java @@ -0,0 +1,90 @@ +package io.agora.sample; + +import io.agora.media.DynamicKey5; +import org.apache.commons.codec.binary.Hex; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Created by liwei on 8/2/17. + */ +public class Verifier5 { + public static void main(String[] args) throws Exception { + if (args.length < 5) { + System.out.println("java io.agora.media.sample.Verifier5 appID appCertificate channelName uid channelKey"); + return; + } + + String appID = args[0]; + String appCertificate = args[1]; + String channelName = args[2]; + int uid = Integer.parseInt(args[3]); + String channelKey = args[4]; + + DynamicKey5 key5 = new DynamicKey5(); + if (! key5.fromString(channelKey)) { + System.out.println("Faile to parse key"); + return ; + } + + System.out.println("signature " + key5.content.signature); + System.out.println("appID " + new String(Hex.encodeHex(key5.content.appID, false))); + System.out.println("unixTs " + key5.content.unixTs); + System.out.println("randomInt " + key5.content.salt); + System.out.println("expiredTs " + key5.content.expiredTs); + System.out.println("extra [" + toString(key5.content.extra) + "]"); + System.out.println("service " + key5.content.serviceType); + + System.out.println(); + System.out.println("Original \t\t " + channelKey); + + if (key5.content.serviceType == DynamicKey5.MEDIA_CHANNEL_SERVICE) { + System.out.println("Uid = 0 \t\t " + DynamicKey5.generateMediaChannelKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, 0, key5.content.expiredTs)); + System.out.println("Uid = " + uid + " \t " + DynamicKey5.generateMediaChannelKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, uid, key5.content.expiredTs)); + } else if (key5.content.serviceType == DynamicKey5.RECORDING_SERVICE) { + System.out.println("Uid = 0 \t\t " + DynamicKey5.generateRecordingKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, 0, key5.content.expiredTs)); + System.out.println("Uid = " + uid + " \t " + DynamicKey5.generateRecordingKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, uid, key5.content.expiredTs)); + } else if (key5.content.serviceType == DynamicKey5.IN_CHANNEL_PERMISSION) { + String permission = key5.content.extra.get(DynamicKey5.ALLOW_UPLOAD_IN_CHANNEL); + if (permission != DynamicKey5.noUpload && permission != DynamicKey5.audioVideoUpload) { + System.out.println("Unknown in channel upload permission " + permission + " in extra [" + toString(key5.content.extra) + "]"); + return ; + } + System.out.println("Uid = 0 \t\t " + DynamicKey5.generateInChannelPermissionKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, 0, key5.content.expiredTs, permission)); + System.out.println("Uid = " + uid + " \t " + DynamicKey5.generateInChannelPermissionKey(appID, appCertificate, channelName, key5.content.unixTs, key5.content.salt, uid, key5.content.expiredTs, permission)); + } else { + System.out.println("Unknown service type " + key5.content.serviceType); + } + + String signature = DynamicKey5.generateSignature(appCertificate, + key5.content.serviceType, + appID, + key5.content.unixTs, + key5.content.salt, + channelName, + uid, + 0, + key5.content.extra + ); + System.out.println("generated signature " + signature); + } + + private static String toString(TreeMap extra) { + String s = ""; + + String separator = ""; + + for (Map.Entry v : extra.entrySet()) { + s += separator; + s += v.getKey(); + s += ":"; + s += v.getValue(); + separator = ", "; + } + + return s; + } + + +} diff --git a/src/main/java/io/agora/signal/SignalingToken.java b/src/main/java/io/agora/signal/SignalingToken.java new file mode 100644 index 0000000..6965d31 --- /dev/null +++ b/src/main/java/io/agora/signal/SignalingToken.java @@ -0,0 +1,33 @@ +package io.agora.signal; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class SignalingToken { + + public static String getToken(String appId, String certificate, String account, int expiredTsInSeconds) throws NoSuchAlgorithmException { + + StringBuilder digest_String = new StringBuilder().append(account).append(appId).append(certificate).append(expiredTsInSeconds); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(digest_String.toString().getBytes()); + byte[] output = md5.digest(); + String token = hexlify(output); + String token_String = new StringBuilder().append("1").append(":").append(appId).append(":").append(expiredTsInSeconds).append(":").append(token).toString(); + return token_String; + } + + public static String hexlify(byte[] data) { + + char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + char[] toDigits = DIGITS_LOWER; + int l = data.length; + char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return String.valueOf(out); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..3a7f635 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +server: + servlet: + context-path: /chat/api + port: 9093 +spring: + datasource: + driver-class-name: org.h2.Driver + schema: classpath:db/schema-h2.sql + data: classpath:db/data-h2.sql + url: jdbc:h2:mem:test + username: root + password: test +jwt: + header: Authorization + secret: linkskk + expiration: 604800 #(60*60*24) + prefix: Bearer +agora: + app-id: 809529cf18814549a0249802512a7508 + app-certificate: ec33f39f9a56486a9b7ddc331394322e + token-expiration: 3600 + privilege-expiration: 3600 \ No newline at end of file diff --git a/src/main/resources/db/data-h2.sql b/src/main/resources/db/data-h2.sql new file mode 100644 index 0000000..26bcb63 --- /dev/null +++ b/src/main/resources/db/data-h2.sql @@ -0,0 +1,4 @@ +DELETE FROM user; +INSERT INTO user (id, username, nickname, email, password) +VALUES (1, 'test1', 'Jone', 'test1@baomidou.com', '123456'), + (2, 'test2', 'Jack', 'test2@baomidou.com', '123456'); \ No newline at end of file diff --git a/src/main/resources/db/schema-h2.sql b/src/main/resources/db/schema-h2.sql new file mode 100644 index 0000000..a929681 --- /dev/null +++ b/src/main/resources/db/schema-h2.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS user; + +CREATE TABLE user +( + id BIGINT(20) NOT NULL COMMENT '主键ID', + username VARCHAR(30) NULL DEFAULT NULL COMMENT '用户名', + nickname VARCHAR(30) NULL DEFAULT NULL COMMENT '昵称', + email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', + password VARCHAR(50) NULL DEFAULT NULL COMMENT '密码', + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/test/java/com/zhangshu/chat/demo/ChatDemoApplicationTests.java b/src/test/java/com/zhangshu/chat/demo/ChatDemoApplicationTests.java new file mode 100644 index 0000000..17421b0 --- /dev/null +++ b/src/test/java/com/zhangshu/chat/demo/ChatDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.zhangshu.chat.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ChatDemoApplicationTests { + + @Test + void contextLoads() { + } + +}