vod普通加密
This commit is contained in:
15
pom.xml
15
pom.xml
@@ -159,6 +159,21 @@
|
||||
<artifactId>spire.doc</artifactId>
|
||||
<version>10.9.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>tea-openapi</artifactId>
|
||||
<version>0.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>tea-console</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>tea-util</artifactId>
|
||||
<version>0.2.21</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
|
||||
215
src/main/java/com/peanut/common/service/HlsDecryptService.java
Normal file
215
src/main/java/com/peanut/common/service/HlsDecryptService.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package com.peanut.common.service;
|
||||
|
||||
import com.aliyun.vod20170321.models.DecryptKMSDataKeyResponseBody;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.peanut.common.utils.PlayToken;
|
||||
import com.peanut.common.utils.SpdbUtil;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.spi.HttpServerProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HlsDecryptService {
|
||||
@Autowired
|
||||
private PlayToken playToken;
|
||||
private static DefaultAcsClient client;
|
||||
|
||||
static {
|
||||
//KMS的区域,必须与视频对应区域
|
||||
String region = "cn-shanghai";
|
||||
// 访问KMS的授权AccessKey信息
|
||||
// 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。
|
||||
// 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。
|
||||
// 本示例通过从环境变量中读取AccessKey,来实现API访问的身份验证。运行代码示例前,请配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
|
||||
String accessKeyId = "LTAI5tMKmWhPfnPsz2J3bfxL";
|
||||
String accessKeySecret = "doFUplbiIxL6PgJME3eSaW8G6HauuC";
|
||||
client = new DefaultAcsClient(DefaultProfile.getProfile(region, accessKeyId, accessKeySecret));
|
||||
}
|
||||
|
||||
/**
|
||||
* 说明:
|
||||
* 1、接收解密请求,获取密文密钥和用户令牌Token
|
||||
* 2、调用KMS decrypt接口获取明文密钥
|
||||
* 3、将明文密钥Base64 decode返回
|
||||
*/
|
||||
public class HlsDecryptHandler implements HttpHandler {
|
||||
/**
|
||||
* 处理解密请求
|
||||
*
|
||||
* @param httpExchange
|
||||
* @throws IOException
|
||||
*/
|
||||
public void handle(HttpExchange httpExchange) throws IOException {
|
||||
String requestMethod = httpExchange.getRequestMethod();
|
||||
// String response;
|
||||
// int statusCode;
|
||||
//
|
||||
// if ("/health".equals(httpExchange.getRequestURI().getPath())) {
|
||||
// response = "OK";
|
||||
// statusCode = 200;
|
||||
// } else {
|
||||
// response = "Hello, this is HlsDecryptHandler!";
|
||||
// statusCode = 200;
|
||||
// }
|
||||
// httpExchange.sendResponseHeaders(statusCode, response.getBytes().length);
|
||||
// OutputStream os = httpExchange.getResponseBody();
|
||||
// os.write(response.getBytes());
|
||||
// os.close();
|
||||
|
||||
if ("GET".equalsIgnoreCase(requestMethod)) {
|
||||
//校验token的有效性
|
||||
String token = getMtsHlsUriToken(httpExchange);
|
||||
boolean validRe = false;
|
||||
try {
|
||||
validRe = playToken.validateToken(token);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (!validRe) {
|
||||
return;
|
||||
}
|
||||
//从URL中取得密文密钥
|
||||
String ciphertext = getCiphertext(httpExchange);
|
||||
if (null == ciphertext)
|
||||
return;
|
||||
//从KMS中解密出来,并Base64 decode
|
||||
byte[] key = decrypt(ciphertext);
|
||||
//设置header
|
||||
setHeader(httpExchange, key);
|
||||
//返回Base64 decode之后的密钥
|
||||
OutputStream responseBody = httpExchange.getResponseBody();
|
||||
responseBody.write(key);
|
||||
responseBody.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException {
|
||||
Headers responseHeaders = httpExchange.getResponseHeaders();
|
||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用KMS decrypt接口解密,并将明文Base64 decode
|
||||
*
|
||||
* @param ciphertext
|
||||
* @return
|
||||
*/
|
||||
private byte[] decrypt(String ciphertext) {
|
||||
DecryptKMSDataKeyResponseBody decryptKMSDataKeyResponseBody = SpdbUtil.enKMS(ciphertext);
|
||||
return Base64.decodeBase64(decryptKMSDataKeyResponseBody.getPlaintext());
|
||||
// DecryptKMSDataKeyRequest request = new DecryptKMSDataKeyRequest();
|
||||
// request.setCipherText(ciphertext);
|
||||
// request.setProtocol(ProtocolType.HTTPS);
|
||||
// try {
|
||||
// DecryptKMSDataKeyResponse response = client.getAcsResponse(request);
|
||||
// String plaintext = response.getPlaintext();
|
||||
// System.out.println("PlainText: " + plaintext);
|
||||
// //注意:需要Base64 decode
|
||||
// return Base64.decodeBase64(plaintext);
|
||||
// } catch (ClientException e) {
|
||||
// e.printStackTrace();
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验令牌有效性
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
private boolean validateToken(String token) {
|
||||
if (null == token || "".equals(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中获取密文密钥参数
|
||||
*
|
||||
* @param httpExchange
|
||||
* @return
|
||||
*/
|
||||
private String getCiphertext(HttpExchange httpExchange) {
|
||||
URI uri = httpExchange.getRequestURI();
|
||||
String queryString = uri.getQuery();
|
||||
String pattern = "CipherText=(\\w*)";
|
||||
Pattern r = Pattern.compile(pattern);
|
||||
Matcher m = r.matcher(queryString);
|
||||
if (m.find())
|
||||
return m.group(1);
|
||||
else {
|
||||
System.out.println("Not Found CipherText Param");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token参数
|
||||
*
|
||||
* @param httpExchange
|
||||
* @return
|
||||
*/
|
||||
private String getMtsHlsUriToken(HttpExchange httpExchange) {
|
||||
URI uri = httpExchange.getRequestURI();
|
||||
String queryString = uri.getQuery();
|
||||
String pattern = "MtsHlsUriToken=(\\w*)";
|
||||
Pattern r = Pattern.compile(pattern);
|
||||
Matcher m = r.matcher(queryString);
|
||||
if (m.find())
|
||||
return m.group(1);
|
||||
else {
|
||||
System.out.println("Not Found MtsHlsUriToken Param");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务启动
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private void serviceBootStrap() throws IOException {
|
||||
HttpServerProvider provider = HttpServerProvider.provider();
|
||||
//监听端口可以自定义,能同时接受最多30个请求
|
||||
HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(8099), 30);
|
||||
httpserver.createContext("/", new HlsDecryptHandler());
|
||||
httpserver.start();
|
||||
// return httpserver;
|
||||
// System.out.println("hls decrypt server started");
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
// HlsDecryptService server = new HlsDecryptService();
|
||||
serviceBootStrap();
|
||||
}
|
||||
|
||||
// @PostConstruct
|
||||
// public static void main(String[] args) throws IOException {
|
||||
// HlsDecryptService server = new HlsDecryptService();
|
||||
// server.serviceBootStrap();
|
||||
// }}
|
||||
}
|
||||
@@ -75,15 +75,15 @@ public class PlayToken {
|
||||
//先校验token的有效时间
|
||||
Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1));
|
||||
// System.out.println("时间校验:" + expireTime);
|
||||
if (System.currentTimeMillis() > expireTime) {
|
||||
return false;
|
||||
}
|
||||
// if (System.currentTimeMillis() > expireTime) {
|
||||
// return false;
|
||||
// }
|
||||
//从DB获取token信息,判断token的有效性,业务方可自行实现
|
||||
VodAesTokenEntity dbToken = getToken(token);
|
||||
//判断是否已经使用过该token
|
||||
if (dbToken == null || dbToken.getUseCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
// if (dbToken == null || dbToken.getUseCount() > 0) {
|
||||
// return false;
|
||||
// }
|
||||
dbToken.setUseCount(1);
|
||||
vodAesTokenDao.updateById(dbToken);
|
||||
//获取到业务属性信息,用于校验
|
||||
|
||||
@@ -3,8 +3,7 @@ package com.peanut.common.utils;
|
||||
import com.aliyun.tea.TeaException;
|
||||
import com.aliyun.teautil.models.RuntimeOptions;
|
||||
import com.aliyun.vod20170321.Client;
|
||||
import com.aliyun.vod20170321.models.GetVideoPlayAuthRequest;
|
||||
import com.aliyun.vod20170321.models.GetVideoPlayAuthResponse;
|
||||
import com.aliyun.vod20170321.models.*;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
@@ -16,8 +15,27 @@ import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;
|
||||
//import org.bytedeco.javacv.FrameGrabber;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SpdbUtil {
|
||||
private static Client client;
|
||||
static {
|
||||
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
|
||||
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
|
||||
.setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
|
||||
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
|
||||
.setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/vod
|
||||
config.endpoint = "vod.cn-shanghai.aliyuncs.com";
|
||||
try {
|
||||
client = new Client(config);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
// private static String accessKeyId = "LTAI5tMKmWhPfnPsz2J3bfxL";
|
||||
// private static String accessKeySecret = "doFUplbiIxL6PgJME3eSaW8G6HauuC";
|
||||
|
||||
// public static Integer getMp4Duration(String url){
|
||||
// double duration = 0;
|
||||
@@ -32,14 +50,14 @@ public class SpdbUtil {
|
||||
// };
|
||||
|
||||
public static GetVideoPlayAuthResponse getPlayAuth(String vid) throws Exception {
|
||||
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
|
||||
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
|
||||
.setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
|
||||
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
|
||||
.setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/vod
|
||||
config.endpoint = "vod.cn-shanghai.aliyuncs.com";
|
||||
Client client = new Client(config);
|
||||
// com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
|
||||
// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
|
||||
// .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
|
||||
// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
|
||||
// .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
|
||||
// // Endpoint 请参考 https://api.aliyun.com/product/vod
|
||||
// config.endpoint = "vod.cn-shanghai.aliyuncs.com";
|
||||
// Client client = new Client(config);
|
||||
GetVideoPlayAuthRequest getVideoPlayAuthRequest = new GetVideoPlayAuthRequest().setVideoId(vid).setAuthInfoTimeout(1000L);;
|
||||
RuntimeOptions runtimeOptions = new RuntimeOptions();
|
||||
try {
|
||||
@@ -68,6 +86,70 @@ public class SpdbUtil {
|
||||
}
|
||||
|
||||
|
||||
public static GenerateKMSDataKeyResponseBody KMS() throws Exception {
|
||||
// com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
|
||||
// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
|
||||
// .setAccessKeyId("LTAI5tMKmWhPfnPsz2J3bfxL")
|
||||
// // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
|
||||
// .setAccessKeySecret("doFUplbiIxL6PgJME3eSaW8G6HauuC");
|
||||
// config.endpoint = "vod.cn-shanghai.aliyuncs.com";
|
||||
// Client client = new Client(config);
|
||||
com.aliyun.vod20170321.models.GenerateKMSDataKeyRequest generateKMSDataKeyRequest = new com.aliyun.vod20170321.models.GenerateKMSDataKeyRequest();
|
||||
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
|
||||
try {
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
GenerateKMSDataKeyResponse generateKMSDataKeyResponse = client.generateKMSDataKeyWithOptions(generateKMSDataKeyRequest, runtime);
|
||||
return generateKMSDataKeyResponse.getBody();
|
||||
} catch (TeaException error) {
|
||||
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
|
||||
// 错误 message
|
||||
System.out.println(error.getMessage());
|
||||
// 诊断地址
|
||||
System.out.println(error.getData().get("Recommend"));
|
||||
com.aliyun.teautil.Common.assertAsString(error.message);
|
||||
return null;
|
||||
} catch (Exception _error) {
|
||||
TeaException error = new TeaException(_error.getMessage(), _error);
|
||||
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
|
||||
// 错误 message
|
||||
System.out.println(error.getMessage());
|
||||
// 诊断地址
|
||||
System.out.println(error.getData().get("Recommend"));
|
||||
com.aliyun.teautil.Common.assertAsString(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static DecryptKMSDataKeyResponseBody enKMS(String kms){
|
||||
com.aliyun.vod20170321.models.DecryptKMSDataKeyRequest decryptKMSDataKeyRequest = new com.aliyun.vod20170321.models.DecryptKMSDataKeyRequest();
|
||||
decryptKMSDataKeyRequest.setCipherText(kms);
|
||||
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
|
||||
try {
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
DecryptKMSDataKeyResponse decryptKMSDataKeyResponse = client.decryptKMSDataKeyWithOptions(decryptKMSDataKeyRequest, runtime);
|
||||
return decryptKMSDataKeyResponse.getBody();
|
||||
} catch (TeaException error) {
|
||||
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
|
||||
// 错误 message
|
||||
System.out.println(error.getMessage());
|
||||
// 诊断地址
|
||||
System.out.println(error.getData().get("Recommend"));
|
||||
com.aliyun.teautil.Common.assertAsString(error.message);
|
||||
return null;
|
||||
} catch (Exception _error) {
|
||||
TeaException error = new TeaException(_error.getMessage(), _error);
|
||||
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
|
||||
// 错误 message
|
||||
System.out.println(error.getMessage());
|
||||
// 诊断地址
|
||||
System.out.println(error.getData().get("Recommend"));
|
||||
com.aliyun.teautil.Common.assertAsString(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static AssumeRoleResponse assumeRole() throws ClientException {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.peanut.modules.common.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import com.peanut.modules.common.entity.VideoM3u8Entity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface VideoM3u8Dao extends MPJBaseMapper<VideoM3u8Entity> {
|
||||
}
|
||||
@@ -38,4 +38,6 @@ public class CourseCatalogueChapterVideoEntity {
|
||||
private String videoUrl;
|
||||
@TableField(exist = false)
|
||||
private String Mp4Url;
|
||||
@TableField(exist = false)
|
||||
private String MtsHlsUriToken;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.peanut.modules.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("video_m3u8")
|
||||
public class VideoM3u8Entity {
|
||||
|
||||
@TableId
|
||||
private Integer id;
|
||||
|
||||
private String vid;
|
||||
|
||||
private String edk;
|
||||
|
||||
private Integer state;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.peanut.common.service.AsyncService;
|
||||
import com.peanut.common.utils.PlayToken;
|
||||
import com.peanut.common.utils.R;
|
||||
import com.peanut.common.utils.ShiroUtils;
|
||||
import com.peanut.common.utils.SpdbUtil;
|
||||
@@ -43,6 +44,8 @@ public class CourseCatalogueChapterVideoServiceImpl extends ServiceImpl<CourseCa
|
||||
private CourseToMedicineDao courseToMedicineDao;
|
||||
@Autowired
|
||||
private MyUserDao userDao;
|
||||
@Autowired
|
||||
private PlayToken playToken;
|
||||
|
||||
@Override
|
||||
public Page getCourseCatalogueChapterVideoList(ParamTo param) {
|
||||
@@ -105,6 +108,8 @@ public class CourseCatalogueChapterVideoServiceImpl extends ServiceImpl<CourseCa
|
||||
GetVideoPlayAuthResponse p = SpdbUtil.getPlayAuth(video.getVideo());
|
||||
String playAuth = p.getBody().getPlayAuth();
|
||||
video.setPlayAuth(playAuth);
|
||||
String s = playToken.generateToken();
|
||||
video.setMtsHlsUriToken(s);
|
||||
}
|
||||
UserCourseVideoPositionEntity videoPosition = getVideoPosition(video, uId);
|
||||
video.setUserCourseVideoPositionEntity(videoPosition);
|
||||
|
||||
@@ -200,11 +200,22 @@ public class CourseController {
|
||||
|
||||
@RequestMapping("/mytt")
|
||||
public R mytt() throws Exception {
|
||||
String s = playToken.generateToken();
|
||||
System.out.println(s);
|
||||
boolean b = playToken.validateToken(s);
|
||||
System.out.println(b);
|
||||
return R.ok();
|
||||
// String s = playToken.generateToken();
|
||||
// System.out.println(s);
|
||||
// boolean b = playToken.validateToken(s);
|
||||
// System.out.println(b);
|
||||
|
||||
GenerateKMSDataKeyResponseBody kms = SpdbUtil.KMS();
|
||||
|
||||
return R.ok().put("result",kms);
|
||||
// return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@RequestMapping("/mytt1")
|
||||
public R mytt1(@RequestBody Map<String,String> map){
|
||||
DecryptKMSDataKeyResponseBody decryptKMSDataKeyResponseBody = SpdbUtil.enKMS(map.get("kms"));
|
||||
return R.ok().put("result",decryptKMSDataKeyResponseBody);
|
||||
}
|
||||
|
||||
@RequestMapping("/ttt")
|
||||
|
||||
Reference in New Issue
Block a user