单车第四天
转自http://coder520.com/
1、跟移动端约定好传输的数据格式:json
1.1、写一个工具类来装我们要返回给移动端的信息
该类有三个属性:状态码code默认200,返回的信息message(如出现异常返回错误信息),返回请求完后返回的数据(使用泛型)
状态吗直接写200不好魔鬼数字,需要定义一个常量类Constants来保存常用的状态码
public class Constants {
/**自定义状态码 start**/
public static final int RESP_STATUS_OK = 200;
public static final int RESP_STATUS_NOAUTH = 401;
public static final int RESP_STATUS_INTERNAL_ERROR = 500;
public static final int RESP_STATUS_BADREQUEST = 400;
/**状态码 end**/
}
@Data
public class ApiResult<T> {
private int code = Constants.RESP_STATUS_OK;
private String message;
private T data;
}
2、实现登录的controller方法,从之前可以知道当用户成功登录后,我们需要返回一个token(类似session),里面含有包含用户的各种信息,但是在返回之前需要接受一个json格式的数据(手机号和验证码还有一个对称加密的key,登录传递过来的数据),然后再通过注解responsebody转换为对象。所以先写一个接受数据的类
@Data
public class LoginInfo {
/**登录信息密文**/
private String data;
/**RSA加密的AES的密钥**/
private String key;
}
api实现,拿到数据之后进行校验,这里校验失败我们不能抛出exception,这是业务逻辑错误,不是系统错误,我们的程序并没有崩溃,所以需要我们自定义我们的exception,如下,校验成功之后调用业务逻辑层的方法并传入参数。
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResult<String> login(@RequestBody LoginInfo loginInfo) {
ApiResult<String> resp = new ApiResult();
try {
//进行校验
String data = loginInfo.getData();
String key = loginInfo.getKey();
if (StringUtils.isBlank(data) || StringUtils.isBlank(key)) {
throw new MaMaBikeException("校验失败!");
}
// 登录成功,返回token
String token = userService.login(data, key);
resp.setData(token);
} catch (MaMaBikeException e) {
//校验失败
log.error(e.getMessage());
resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
resp.setMessage(e.getMessage());
} catch (Exception e) {
// 登录失败,返回失败信息,就不用返回data
// 记录日志
log.error("Fail to login", e);
resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
resp.setMessage("内部错误!");
}
return resp;
}
public class MaMaBikeException extends Exception {
public MaMaBikeException(String message){
super(message);
}
public int getStatusCode(){
return Constants.RESP_STATUS_INTERNAL_ERROR;
}
}
业务逻辑方法,先进行解密之后校验,然后使用fastjson去转换前端传过来的json数据。
/**
* Author ljs
* Description 登录业务
* Date 2018/9/3 23:01
**/
@Override
public String login(String data, String key) throws MaMaBikeException {
String decryptData = null;
String token = null;
try {
//RSA解密AES的key
byte[] aesKey = RSAUtil.decryptByPrivateKey(Base64Util.decode(key));
//AES的key解密AES加密数据
decryptData = AESUtil.decrypt(data, new String(aesKey, "utf-8"));
if (decryptData == null) {
throw new Exception();
}
//解密成功后,使用fastjson转为对象,因为移动端传过来的是json数据
JSONObject jsonObject = JSON.parseObject(decryptData);
String mobile = jsonObject.getString("mobile"); //电话
String code = jsonObject.getString("code"); //验证码
String platform = jsonObject.getString("platform"); //机器类型
//String channelId = jsonObject.getString("channelId"); //推送频道编码, 单个设备唯一
//转换为json对象获取值后进行校验
if(StringUtils.isBlank(mobile)|| StringUtils.isBlank(code)||
StringUtils.isBlank(platform)){
throw new Exception();
}
//去redis取验证码比较手机号码和验证码是否匹配 若匹配 说明是本人手机
//判断用户是否存在数据库,如果存在,生成token,存入redis,如果不存在,帮他注册,插入数据库
} catch (Exception e) {
log.error("Fail to decypt data", e);
//传给移动端
throw new MaMaBikeException("数据解析错误!");
}
return null;
}
使用postman测试是否解密成功,先模拟移动端加密数据和加密key
/**AES加密数据,客户端操作开始**/
String key = "123456789abcdefg"; //约定好的key
String result = "{'mobile':'18319830032','code':'6666','platform':'android'}";
//传输的数据
String enResult = encrypt(result, key); //加密
System.out.println(enResult);
/**RSA加密AES的密钥,客户端操作结束**/
byte[] enKey = RSAUtil.encryptByPublicKey(key.getBytes(), "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCEPB4Y7bd4ttV3phsm7VpR lmG0j19QUQWRG+MVCgw7f7ahvgwiXpwrqWP4hyZFxlFRUT4PlS11cKNut1Qm xjco1pYIxZUG6TfQj+a9rnUOGogdkyS76IpKi5/xal6MTmPqlfpE9SkBLvDc qLFX8FBo0+/ReoPrIPg3H4Saj99tOwIDAQAB");
//需要再转码不然在http传输会出问题,因为上面输出乱码
String baseKey = Base64Util.encode(enKey);
System.out.println(baseKey);
//加密后的数据
WLixCdk7c4m13V9lmWG1LEZsQoZSGKAdZvJzzBnTOSi1oJLSj/8RVq55c4d+ ekmkG6ak+zJInfUT5qZMUnlD8w==
//加密后的key
PCwEbSCsguOzH7XtGD/dUb09DDoZUROJ1m60JPYXYWBYHA+1HM4aEqjNZsde +u1CURklVsw203kdZihwmb0eI7x1DIWB9KdZhMHK1jAA+rPeAhXUvFxblj8w l39cIgyErSqoK5YOjM71zeKKmEvPxn8xoCfh6WYj9fHouExeycY=
image.png
image.png
ok,成功拿到。
从jsonobject拿到验证码后,去redis里看看发送之前存的验证码是否与传过来的匹配,为什么要去redis取验证码呢,凡是有过期的东西都可以使用redis的key来使用,因为redis的key有过期事件。
首先整合redis,不要使用springboot提供的,还是使用jedis。
pom加入依赖
<!--整合jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
接着配置端口啊什么的,不要写在基础配置里,因为端口在开发和产品中可能不同,所以这里redis的配置写在dev的配置文件里。空闲连接5,总共连接10,超时时间3秒
#reids
redis:
host: 127.0.0.1
port: 6379
auth:
max-idle: 5
max-total: 10
max-wait-millis: 3000
写个工具包,专门操作redis,两个类一个redis连接池,一个redis操作。然后初始化连接池需要去读取dev配置文件,如果很多类需要去读取配置文件里的值,到处注入,不好,我们可以干脆创建一个参数类,里面存放配置文件里的@Value值,然后以后修改也在这个类里修改就行。
@Data
@Component
public class Parameters {
/*****redis config start*******/
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Value("${redis.auth}")
private String redisAuth;
@Value("${redis.max-idle}")
private int redisMaxTotal;
@Value("${redis.max-total}")
private int redisMaxIdle;
@Value("${redis.max-wait-millis}")
private int redisMaxWaitMillis;
/*****redis config end*******/
}
现在只需要注入parameters就行,redis包装类三段,初始化连接池,添加配置参数,返回这个连接池实例,但是还是有一个问题,当实例化一个连接池的时候,怎么确保init方法一定会执行,可以使用spring的一个注解postConstruct,这个注解好比静态代码块,这样就不用再getjedispool方法里调用init方法了。
@Component
@Slf4j
public class JedisPoolWrapper {
private JedisPool jedisPool = null;
@Autowired
private Parameters parameters;
@PostConstruct
public void init() throws MaMaBikeException {
try{
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(parameters.getRedisMaxTotal());
config.setMaxIdle(parameters.getRedisMaxIdle());
config.setMaxWaitMillis(parameters.getRedisMaxWaitMillis());
jedisPool = new JedisPool(config,parameters.getRedisHost(),parameters.getRedisPort(),2000,parameters.getRedisAuth());
}catch (Exception e){
log.error("Fail to initialize jedis pool", e);
throw new MaMaBikeException("Fail to initialize jedis pool");
}
}
public JedisPool getJedisPool(){
return jedisPool;
}
}
@Autowired
private JedisPoolWrapper jedisPoolWrapper;
/**
* 缓存 可以value 永久
*
* @param key
* @param value
*/
public void cache(String key, String value) {
try {
JedisPool pool = jedisPoolWrapper.getJedisPool();
if (pool != null) {
try (Jedis Jedis = pool.getResource()) {
Jedis.select(0); //选择redis第0片区
Jedis.set(key, value);
}
}
} catch (Exception e) {
log.error("Fail to cache value", e);
}
}
/**
* 获取缓存key
*
* @param key
* @return
*/
public String getCacheValue(String key) {
String value = null;
try {
JedisPool pool = jedisPoolWrapper.getJedisPool();
if (pool != null) {
try (Jedis Jedis = pool.getResource()) {
Jedis.select(0);
value = Jedis.get(key);
}
}
} catch (Exception e) {
log.error("Fail to get cached value", e);
}
return value;
}
/**
* 设置key value 以及过期时间
*
* @param key
* @param value
* @param expiry
* @return
*/
public long cacheNxExpire(String key, String value, int expiry) {
long result = 0;
try {
JedisPool pool = jedisPoolWrapper.getJedisPool();
if (pool != null) {
try (Jedis jedis = pool.getResource()) {
jedis.select(0);
result = jedis.setnx(key, value);
jedis.expire(key, expiry);
}
}
} catch (Exception e) {
log.error("Fail to cacheNx value", e);
}
return result;
}
/**
* 删除缓存key
*
* @param key
*/
public void delKey(String key) {
JedisPool pool = jedisPoolWrapper.getJedisPool();
if (pool != null) {
try (Jedis jedis = pool.getResource()) {
jedis.select(0);
try {
jedis.del(key);
} catch (Exception e) {
log.error("Fail to remove key from redis", e);
}
}
}
}
整合好后,继续写业务逻辑
去redis取验证码比较手机号码和验证码是否匹配 若匹配 说明是本人手机,用户不存在,帮他注册
@Autowired
private CommonCacheUtil cacheUtil;
//去redis取验证码比较手机号码和验证码是否匹配 若匹配 说明是本人手机
String verCode = cacheUtil.getCacheValue(mobile);
User user = null;
///用code去匹配verCode,因为code上面已经验证过是不为null,而v可能为null,null.equals空指针异常
if (code.equals(verCode)) {
//手机匹配
user = userMapper.selectByMobile(mobile);
if(user==null){
//用户不存在,帮他注册
user = new User();
user.setMobile(mobile);
user.setNickname(mobile); //默认是手机号
userMapper.insertSelective(user);
}
}else {
throw new MaMaBikeException("验证码或者手机号不匹配");
}
<select id="selectByMobile" resultMap="BaseResultMap" parameterType="java.lang.String" >
select
<include refid="Base_Column_List" />
from user
where mobile = #{mobile}
</select>
生成token
//生成token
try{
token = this.generateToken(user);
}catch (Exception e){
throw new MaMaBikeException("fail.to.generate.token");
}
使用用户id和iphone和系统当前时间然后md5加密生成token
/**
* Author ljs
* Description 生成唯一标识token,并且把token加密
* Date 2018/9/4 15:04
**/
private String generateToken(User user)
throws Exception {
String source = user.getId() + ":" + user.getMobile() + System.currentTimeMillis();
return MD5Util.getMD5(source);
}
在存入redis之前,token作为key,value是用户的信息,但是我们原本的user实体类里的属性不太够,所以创建一个新的实体类userElement,而且我们value是一个对象,使用的往redis存map,所以需要两个方法,map转对象,对象转map
/**
* Author ljs
* Description 用于缓存的user信息体
* Date 2018/9/4 15:09
**/
@Data
public class UserElement {
private long userId;
private String mobile;
private String token;
private String platform; //ios或者andriod
private String pushUserId; //单设备推送标识
private String pushChannelId; //所以设备推送标识
/**
* 转 map
* @return
*/
public Map<String, String> toMap() {
Map<String, String> map = new HashMap<String, String>();
map.put("platform", this.platform);
map.put("userId", this.userId + "");
map.put("token", token);
map.put("mobile", mobile);
if (this.pushUserId != null) {
map.put("pushUserId", this.pushUserId);
}
if (this.pushChannelId != null) {
map.put("pushChannelId", this.pushChannelId);
}
return map;
}
/**
* map转对象
* @param map
* @return
*/
public static UserElement fromMap(Map<String, String> map) {
UserElement ue = new UserElement();
ue.setPlatform(map.get("platform"));
ue.setToken(map.get("token"));
ue.setMobile(map.get("mobile"));
ue.setUserId(Long.parseLong(map.get("userId")));
ue.setPushUserId(map.get("pushUserId"));
ue.setPushChannelId(map.get("pushChannelId"));
return ue;
}
}
redis操作方法需要添加一个当登录时往redis存哈希的方法,之前userElement为什么要加入token属性就是为了在这里能取出来然后设置为该哈希的key,而设置userid是为了获取token,先获取token之后再根据token去获取用户。
/**
* 登录时设置token
* @param ue
*/
public void putTokenWhenLogin(UserElement ue) {
JedisPool pool = jedisPoolWrapper.getJedisPool();
if (pool != null) {
try (Jedis jedis = pool.getResource()) {
jedis.select(0);
Transaction trans = jedis.multi();
try {
//重新设置token
trans.del(TOKEN_PREFIX + ue.getToken());
//token为key,用户信息转为map之后为value
trans.hmset(TOKEN_PREFIX + ue.getToken(), ue.toMap());
//设置超时时间3天
trans.expire(TOKEN_PREFIX + ue.getToken(), 2592000);
//sadd将多个token存入一个集合key中
//因为该用户可能在多个设备登录有多个token,我们需要提醒一下
trans.sadd(USER_PREFIX + ue.getUserId(), ue.getToken());
trans.exec();
} catch (Exception e) {
trans.discard();
log.error("Fail to cache token to redis", e);
}
}
}
}
最后存入redis,并且返回给移动端生成的token就行了
/**存入redis**/
UserElement ue = new UserElement();
ue.setMobile(mobile);
ue.setUserId(user.getId());
ue.setToken(token);
ue.setPlatform(platform);
// ue.setPushChannelId(channelId);
cacheUtil.putTokenWhenLogin(ue);
return token;
启动服务器验证,先往redis存一个18319830032,6666用于验证
然后发送
image.png
这里发送请求的时候报了一个错,百度了说请求用http,我也是用了http,后面再解决吧,这里主要就是实现生成token,存入redis,报错没影响就算了。
Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: Invalid character found in method name. HTTP method names must be tokens
ok返回成功并且也确实存入到redis里。
image.png
image.png