单车

单车第四天

2018-09-25  本文已影响23人  shenyoujian

转自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
上一篇下一篇

猜你喜欢

热点阅读