Commit 6e6c9d1b authored by 技术部-任文超's avatar 技术部-任文超

提交jUnit,当前Demo为单次令牌切面的测试用例,已绿

parent c39f7a6b
...@@ -12,7 +12,7 @@ public interface Constants { ...@@ -12,7 +12,7 @@ public interface Constants {
String PASSWORD_SALT = "_lkb"; String PASSWORD_SALT = "_lkb";
String IMAGE_CAPTCHA_KEY = "img_captcha:"; String IMAGE_CAPTCHA_KEY = "img_captcha:";
String TOKEN_SINGLE_KEY_FOR_PHONE = "token_single:for_phone:"; String TOKEN_ONCE_KEY_FOR_PHONE = "token_once:for_phone:";
String REDIS_CAPTCHA_KEY = "auth:"; String REDIS_CAPTCHA_KEY = "auth:";
String REDIS_CAPTCHA_KEY_PATTERN = REDIS_CAPTCHA_KEY + IMAGE_CAPTCHA_KEY + "*"; String REDIS_CAPTCHA_KEY_PATTERN = REDIS_CAPTCHA_KEY + IMAGE_CAPTCHA_KEY + "*";
......
...@@ -4,7 +4,6 @@ import cn.quantgroup.xyqb.Constants; ...@@ -4,7 +4,6 @@ import cn.quantgroup.xyqb.Constants;
import cn.quantgroup.xyqb.exception.VerificationCodeErrorException; import cn.quantgroup.xyqb.exception.VerificationCodeErrorException;
import cn.quantgroup.xyqb.model.JsonResult; import cn.quantgroup.xyqb.model.JsonResult;
import cn.quantgroup.xyqb.util.ValidationUtil; import cn.quantgroup.xyqb.util.ValidationUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
...@@ -22,6 +21,8 @@ import org.springframework.web.context.request.ServletRequestAttributes; ...@@ -22,6 +21,8 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
...@@ -35,9 +36,9 @@ import java.util.Objects; ...@@ -35,9 +36,9 @@ import java.util.Objects;
*/ */
@Aspect @Aspect
@Component @Component
public class SingleTokenValidateAdvisor { public class TokenOnceValidateAdvisor {
private static final Logger LOGGER = LoggerFactory.getLogger(SingleTokenValidateAdvisor.class); private static final Logger LOGGER = LoggerFactory.getLogger(TokenOnceValidateAdvisor.class);
@Autowired @Autowired
@Qualifier("stringRedisTemplate") @Qualifier("stringRedisTemplate")
...@@ -46,14 +47,14 @@ public class SingleTokenValidateAdvisor { ...@@ -46,14 +47,14 @@ public class SingleTokenValidateAdvisor {
/** /**
* 自动化测试忽略单次令牌校验 * 自动化测试忽略单次令牌校验
*/ */
@Value("${xyqb.auth.singletoken.autotest.enable:false}") @Value("${xyqb.auth.tokenonce.autotest.enable:true}")
private boolean autoTestSingleTokenEnabled; private boolean autoTestTokenOnceEnabled;
/** /**
* 单次令牌校验切面 * 单次令牌校验切面
*/ */
@Pointcut("@annotation(cn.quantgroup.xyqb.aspect.token.SingleTokenValidator)") @Pointcut("@annotation(cn.quantgroup.xyqb.aspect.token.TokenOnceValidator)")
private void needSingleTokenValidate() { private void needTokenOnceValidate() {
} }
/** /**
...@@ -61,9 +62,9 @@ public class SingleTokenValidateAdvisor { ...@@ -61,9 +62,9 @@ public class SingleTokenValidateAdvisor {
* *
* @throws Throwable * @throws Throwable
*/ */
@Around("needSingleTokenValidate()") @Around("needTokenOnceValidate()")
private Object doSingleTokenValidate(ProceedingJoinPoint pjp) throws Throwable { private Object doTokenOnceValidate(ProceedingJoinPoint pjp) throws Throwable {
if (autoTestSingleTokenEnabled) { if (autoTestTokenOnceEnabled) {
return pjp.proceed(); return pjp.proceed();
} }
boolean checkTokenForPhone = checkTokenForPhone(); boolean checkTokenForPhone = checkTokenForPhone();
...@@ -85,27 +86,27 @@ public class SingleTokenValidateAdvisor { ...@@ -85,27 +86,27 @@ public class SingleTokenValidateAdvisor {
} }
// 当前用户手机号 // 当前用户手机号
String phoneNo = phoneTokenMap.get("phoneNo"); String phoneNo = phoneTokenMap.get("phoneNo");
// 当前请求的SingleToken // 当前请求的TokenOnce
String requestToken = phoneTokenMap.get("requestToken"); String requestToken = phoneTokenMap.get("requestToken");
if (StringUtils.isBlank(phoneNo) || StringUtils.isBlank(requestToken)){ if (StringUtils.isBlank(phoneNo) || StringUtils.isBlank(requestToken)){
return false; return false;
} }
final String key = Constants.TOKEN_SINGLE_KEY_FOR_PHONE + phoneNo; final String key = Constants.TOKEN_ONCE_KEY_FOR_PHONE + phoneNo;
String singleToken = redisTemplate.opsForValue().get(key); String tokenOnce = redisTemplate.opsForValue().get(key);
// SingleToken不应为空值(空白、空格、null) // TokenOnce不应为空值(空白、空格、null)
if (StringUtils.isBlank(singleToken)) { if (StringUtils.isBlank(tokenOnce)) {
// 修正规则 // 修正规则
if(redisTemplate.hasKey(key)){ if(redisTemplate.hasKey(key)){
redisTemplate.delete(key); redisTemplate.delete(key);
} }
return false; return false;
} }
boolean valid = Objects.equals(singleToken, requestToken); boolean valid = Objects.equals(tokenOnce, requestToken);
// SingleToken校验正确时删除key // TokenOnce校验正确时删除key
if(valid) { if(valid) {
redisTemplate.delete(key); redisTemplate.delete(key);
}else { }else {
LOGGER.info("Token过期,请重新请求, token_single:for_phone:={}, requestToken={}, clientIp={}", phoneNo, requestToken, request.getRemoteAddr()); LOGGER.info("Token过期,请重新请求, token_once:for_phone:={}, requestToken={}, clientIp={}", phoneNo, requestToken, request.getRemoteAddr());
} }
return valid; return valid;
} }
...@@ -113,25 +114,19 @@ public class SingleTokenValidateAdvisor { ...@@ -113,25 +114,19 @@ public class SingleTokenValidateAdvisor {
/** /**
* 单次令牌参数解析 * 单次令牌参数解析
* *
* @param request 当前请求,其首部行必须包含形如【SingleToken 13461067662:0123456789abcdef】的UTF-8编码的Base64加密参数 * @param request 当前请求,其首部行必须包含形如【TokenOnce MTM0NjEwNjc2NjI6NmFjMDY2NWItZTE5Yy00MzkyLWEyNDQtN2I2MTY5MDgzM2Y1】的UTF-8编码的Base64加密参数
* @return 令牌参数Map 或 null * @return 令牌参数Map 或 null
*/ */
private Map<String, String> getHeaderParam(HttpServletRequest request) { private Map<String, String> getHeaderParam(HttpServletRequest request) {
String verificationHeader = "SingleToken "; String headerName = "TokenOnce";
String credential = request.getHeader("authorization"); String credential = request.getHeader(headerName);
if (StringUtils.isBlank(credential) || !credential.startsWith(verificationHeader)) { if (StringUtils.isBlank(credential)) {
LOGGER.info("令牌参数无效, credential:{}", credential); LOGGER.info("令牌参数无效, credential:{}", credential);
return null; return null;
} }
credential = credential.substring(verificationHeader.length(), credential.length());
boolean headerParamValid = true; boolean headerParamValid = true;
byte[] buf = Base64.decodeBase64(credential); byte[] buf = Base64.getDecoder().decode(credential.getBytes());
try { credential = new String(buf, Charset.forName("UTF-8"));
credential = new String(buf, "UTF-8");
} catch (UnsupportedEncodingException e) {
headerParamValid = false;
LOGGER.error("不支持的编码{}.", credential, e);
}
if(!headerParamValid){ if(!headerParamValid){
LOGGER.info("令牌参数无效, credential:{}", credential); LOGGER.info("令牌参数无效, credential:{}", credential);
return null; return null;
...@@ -144,7 +139,7 @@ public class SingleTokenValidateAdvisor { ...@@ -144,7 +139,7 @@ public class SingleTokenValidateAdvisor {
} }
// 当前用户手机号 // 当前用户手机号
String phoneNo = credentialArr[0]; String phoneNo = credentialArr[0];
// 当前请求的SingleToken // 当前请求的TokenOnce
String requestToken = credentialArr[1]; String requestToken = credentialArr[1];
headerParamValid = headerParamValid && ValidationUtil.validatePhoneNo(phoneNo); headerParamValid = headerParamValid && ValidationUtil.validatePhoneNo(phoneNo);
if (!headerParamValid) { if (!headerParamValid) {
......
...@@ -11,5 +11,5 @@ import java.lang.annotation.*; ...@@ -11,5 +11,5 @@ import java.lang.annotation.*;
@Documented @Documented
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface SingleTokenValidator { public @interface TokenOnceValidator {
} }
...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller.internal.token; ...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller.internal.token;
import cn.quantgroup.xyqb.Constants; import cn.quantgroup.xyqb.Constants;
import cn.quantgroup.xyqb.controller.IBaseController; import cn.quantgroup.xyqb.controller.IBaseController;
import cn.quantgroup.xyqb.model.JsonResult; import cn.quantgroup.xyqb.model.JsonResult;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; ...@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
...@@ -26,9 +28,9 @@ import java.util.concurrent.TimeUnit; ...@@ -26,9 +28,9 @@ import java.util.concurrent.TimeUnit;
*/ */
@RestController @RestController
@RequestMapping("/token") @RequestMapping("/token")
public class SingleTokenController implements IBaseController { public class TokenOnceController implements IBaseController {
private static final Logger LOGGER = LoggerFactory.getLogger(SingleTokenController.class); private static final Logger LOGGER = LoggerFactory.getLogger(TokenOnceController.class);
private static final Long ONE_HOUR = 1 * 60 * 60L; private static final Long ONE_HOUR = 1 * 60 * 60L;
@Autowired @Autowired
...@@ -36,19 +38,22 @@ public class SingleTokenController implements IBaseController { ...@@ -36,19 +38,22 @@ public class SingleTokenController implements IBaseController {
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
/** /**
* 向指定用户账号(手机号)发放一枚SingleToken * 向指定用户账号(手机号)发放一枚TokenOnce
* TokenOnce用法:其首部行必须包含形如【TokenOnce MTM0NjEwNjc2NjI6NmFjMDY2NWItZTE5Yy00MzkyLWEyNDQtN2I2MTY5MDgzM2Y1】的UTF-8编码的Base64加密参数
* 例如:Base64.getEncoder().encodeToString("13461067662:6ac0665b-e19c-4392-a244-7b61690833f5".getBytes(Charset.forName("UTF-8")));
*
* @param phoneNo 用户账号(手机号) * @param phoneNo 用户账号(手机号)
* @return 单次令牌 * @return 单次令牌
*/ */
@RequestMapping(value = "/single") @RequestMapping(value = "/once")
public JsonResult newSingleToken(HttpServletRequest request, @ModelAttribute("phoneNo") String phoneNo) { public JsonResult newTokenOnce(HttpServletRequest request, @ModelAttribute("phoneNo") String phoneNo) {
if (StringUtils.isBlank(phoneNo)){ if (StringUtils.isBlank(phoneNo)){
return JsonResult.buildErrorStateResult("", "fail"); return JsonResult.buildErrorStateResult("获取TokenOnce失败", "");
} }
String singleToken = UUID.randomUUID().toString(); String tokenOnce = UUID.randomUUID().toString();
final String key = Constants.TOKEN_SINGLE_KEY_FOR_PHONE + phoneNo; final String key = Constants.TOKEN_ONCE_KEY_FOR_PHONE + phoneNo;
redisTemplate.opsForValue().set(key, singleToken, ONE_HOUR, TimeUnit.SECONDS); redisTemplate.opsForValue().set(key, tokenOnce, ONE_HOUR, TimeUnit.SECONDS);
return JsonResult.buildSuccessResult("", singleToken); return JsonResult.buildSuccessResult("", tokenOnce);
} }
} }
...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller.internal.user; ...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller.internal.user;
import cn.quantgroup.xyqb.Constants; import cn.quantgroup.xyqb.Constants;
import cn.quantgroup.xyqb.aspect.captcha.CaptchaNewValidator; import cn.quantgroup.xyqb.aspect.captcha.CaptchaNewValidator;
import cn.quantgroup.xyqb.aspect.logcaller.LogHttpCaller; import cn.quantgroup.xyqb.aspect.logcaller.LogHttpCaller;
import cn.quantgroup.xyqb.aspect.token.TokenOnceValidator;
import cn.quantgroup.xyqb.controller.IBaseController; import cn.quantgroup.xyqb.controller.IBaseController;
import cn.quantgroup.xyqb.entity.Merchant; import cn.quantgroup.xyqb.entity.Merchant;
import cn.quantgroup.xyqb.entity.User; import cn.quantgroup.xyqb.entity.User;
...@@ -296,6 +297,7 @@ public class UserController implements IBaseController { ...@@ -296,6 +297,7 @@ public class UserController implements IBaseController {
* @param channelId * @param channelId
* @return * @return
*/ */
@TokenOnceValidator
@RequestMapping("/register") @RequestMapping("/register")
public JsonResult register(@RequestParam String phoneNo, @RequestParam String password, public JsonResult register(@RequestParam String phoneNo, @RequestParam String password,
@RequestParam String verificationCode, @RequestParam(required = false) Long channelId, @RequestParam String verificationCode, @RequestParam(required = false) Long channelId,
......
...@@ -58,7 +58,7 @@ jr58.notify.userinfo=http://xfd.test.58v5.cn/customer/quantgroup_user_info ...@@ -58,7 +58,7 @@ jr58.notify.userinfo=http://xfd.test.58v5.cn/customer/quantgroup_user_info
# 是否启用超级验证码 "__SUPERQG__", 用于测试环境自动化测试, 线上环境可忽略此参数 # 是否启用超级验证码 "__SUPERQG__", 用于测试环境自动化测试, 线上环境可忽略此参数
xyqb.auth.captcha.super.enable=1 xyqb.auth.captcha.super.enable=1
# 单次令牌验证, 用于测试环境自动化测试, 线上环境可忽略此参数 # 单次令牌验证, 用于测试环境自动化测试, 线上环境可忽略此参数
xyqb.auth.singletoken.autotest.enable=true xyqb.auth.tokenonce.autotest.enable=false
#首参数校验 #首参数校验
xyqb.fplock.limit.byhour=3 xyqb.fplock.limit.byhour=3
......
...@@ -39,8 +39,6 @@ jr58.notify.userinfo=http://xfd.test.58v5.cn/customer/quantgroup_user_info ...@@ -39,8 +39,6 @@ jr58.notify.userinfo=http://xfd.test.58v5.cn/customer/quantgroup_user_info
# 图形验证码 # 图形验证码
# 是否启用超级验证码 "__SUPERQG__", 用于测试环境自动化测试, 线上环境可忽略此参数 # 是否启用超级验证码 "__SUPERQG__", 用于测试环境自动化测试, 线上环境可忽略此参数
xyqb.auth.captcha.super.enable=1 xyqb.auth.captcha.super.enable=1
# 单次令牌验证, 用于测试环境自动化测试, 线上环境可忽略此参数
xyqb.auth.singletoken.autotest.enable=true
#首参数校验 #首参数校验
xyqb.fplock.limit.byhour=3 xyqb.fplock.limit.byhour=3
xyqb.fplock.limit.byday=5 xyqb.fplock.limit.byday=5
......
package token;
import cn.quantgroup.xyqb.Bootstrap;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.nio.charset.Charset;
import java.util.Base64;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Bootstrap.class)
@WebAppConfiguration
public class TokenOnceTests {
final String phoneNo = "13461067662";
private MockMvc mvc;
@Autowired
WebApplicationContext webApplicationConnect;
@Before
public void setUp() throws JsonProcessingException {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationConnect).build();
}
/**
* 测试Server是否可达
* @throws Exception
*/
@Test
public void testServer() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
/**
* 测试TokenOnce发放服务
* @throws Exception
*/
@Test
public void testTokenOnce() throws Exception{
String tokenOnceUri = "/token/once";
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get(tokenOnceUri).accept(MediaType.APPLICATION_JSON)
.header("Session-Id", "82107d0326772b8b5c72ec11801b8ab3"))
.andExpect(status().isOk())
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
JSONObject jsonResult = JSON.parseObject(new String(content));
Object code = jsonResult.get("code");
Assert.assertEquals("0000", code);
Object data = jsonResult.get("data");
Assert.assertEquals(data, "");
Object msg = jsonResult.get("msg");
Assert.assertEquals("获取TokenOnce失败", jsonResult.get("msg"));
jsonResult = JSON.parseObject(new String(content));
mvcResult = mvc.perform(MockMvcRequestBuilders.get(tokenOnceUri).accept(MediaType.APPLICATION_JSON)
.param("phoneNo", phoneNo))
.andExpect(status().isOk())
.andReturn();
content = mvcResult.getResponse().getContentAsString();
jsonResult = JSON.parseObject(new String(content));
code = jsonResult.get("code");
Assert.assertEquals("0000", code);
data = jsonResult.get("data");
Assert.assertNotNull(data);
msg = jsonResult.get("msg");
Assert.assertEquals(msg, "");
}
/**
* 测试TokenOnce切面
* @throws Exception
*/
@Test
public void testAspect() throws Exception{
// 获取TokenOnce
String tokenOnceUri = "/token/once";
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get(tokenOnceUri).accept(MediaType.APPLICATION_JSON)
.param("phoneNo", phoneNo))
.andExpect(status().isOk())
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
JSONObject jsonResult = JSON.parseObject(new String(content));
Object code = jsonResult.get("code");
Assert.assertEquals("0000", code);
Object data = jsonResult.get("data");
Assert.assertNotNull(data);
StringBuilder tokenBuilder = new StringBuilder(phoneNo);
String tokenOnce = new String(Base64.getEncoder().encodeToString(tokenBuilder.append(":").append(data).toString().getBytes(Charset.forName("UTF-8"))));
// 第一次使用TokenOnce
String aspectUri = "/user/register";
mvcResult = mvc.perform(MockMvcRequestBuilders.get(aspectUri).accept(MediaType.APPLICATION_JSON)
.header("TokenOnce", tokenOnce)
.param("phoneNo", phoneNo)
.param("password", "Qg123456")
.param("verificationCode", "1234"))
.andExpect(status().isOk())
.andReturn();
content = mvcResult.getResponse().getContentAsString();
jsonResult = JSON.parseObject(new String(content));
code = jsonResult.get("code");
Object businessCode = jsonResult.get("businessCode");
Assert.assertEquals("0000", code);
Assert.assertNotEquals("0002", businessCode);
// 使用过期的TokenOnce
mvcResult = mvc.perform(MockMvcRequestBuilders.get(aspectUri).accept(MediaType.APPLICATION_JSON)
.header("TokenOnce", tokenOnce)
.param("phoneNo", phoneNo)
.param("password", "Qg123456")
.param("verificationCode", "1234"))
.andExpect(status().isOk())
.andReturn();
content = mvcResult.getResponse().getContentAsString();
jsonResult = JSON.parseObject(new String(content));
code = jsonResult.get("code");
Assert.assertEquals("0000", code);
businessCode = jsonResult.get("businessCode");
Assert.assertEquals("0002", businessCode);
// 不使用TokenOnce
mvcResult = mvc.perform(MockMvcRequestBuilders.get(aspectUri).accept(MediaType.APPLICATION_JSON)
.param("phoneNo", phoneNo)
.param("password", "Qg123456")
.param("verificationCode", "1234"))
.andExpect(status().isOk())
.andReturn();
content = mvcResult.getResponse().getContentAsString();
jsonResult = JSON.parseObject(new String(content));
code = jsonResult.get("code");
Assert.assertEquals("0000", code);
businessCode = jsonResult.get("businessCode");
Assert.assertEquals("0002", businessCode);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment