乐趣区

基于redis的小程序登录实现

基于 redis 的小程序登录实现

作者:gigass
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

** 你好, 这是我的第一篇博客.
因为前段时间做过一个小程序, 所以去学习了一下小程序的登录流程. 废话不多说, 下面附上我的学习结果.**
这张图是小程序的登录流程解析:

小程序登陆授权流程:

  1. 在小程序端调用 wx.login()获取 code, 由于我是做后端开发的这边不做赘述, 直接贴上代码了. 有兴趣的直接去官方文档看下, 链接放这里: wx.login()
wx.login({success (res) {if (res.code) {
      // 发起网络请求
      wx.request({
        url: 'https://test.com/onLogin',
        data: {code: res.code}
      })
    } else {console.log('登录失败!' + res.errMsg)
    }
  }
})

小程序前端登录后会获取 code, 调用自己的开发者服务接口, 调用个 get 请求:

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

需要得四个参数:
appid:小程序 appid
secret:小程序密钥
js_code:刚才获取的 code
grant_type:’authorization_code’ // 这个是固定的

如果不出意外的话, 微信接口服务器会返回四个参数:

详情可以看下官方文档: jscode2session

下面附上我的代码:

 @AuthIgnore
    @RequestMapping("/login")
    @ResponseBody
    public ResponseBean openId(@RequestParam(value = "code", required = true) String code,
                               @RequestParam(value = "avatarUrl") String avatarUrl,
                               @RequestParam(value = "city") String city,
                               @RequestParam(value = "country") String country,
                               @RequestParam(value = "gender") String gender,
                               @RequestParam(value = "language") String language,
                               @RequestParam(value = "nickName") String nickName,
                               @RequestParam(value = "province") String province,
                               HttpServletRequest request) { // 小程序端获取的 CODE
        ResponseBean responseBean = new ResponseBean();
        try {boolean check = (StringUtils.isEmpty(code)) ? true : false;
            if (check) {responseBean = new ResponseBean(false, UnicomResponseEnums.NO_CODE);
                return responseBean;
            }
            // 将获取的用户数据存入数据库;
            Map<String, Object> msgs = new HashMap<>();
            msgs.put("appid", appId);
            msgs.put("secret", secret);
            msgs.put("js_code", code);
            msgs.put("grant_type", "authorization_code");
            // java 的网络请求,返回字符串
            String data = HttpUtils.get(msgs, Constants.JSCODE2SESSION);
            logger.info("======>" + data);
            String openId = JSONObject.parseObject(data).getString("openid");
            String session_key = JSONObject.parseObject(data).getString("session_key");
            String unionid = JSONObject.parseObject(data).getString("unionid");
            String errcode = JSONObject.parseObject(data).getString("errcode");
            String errmsg = JSONObject.parseObject(data).getString("errmsg");

            JSONObject json = new JSONObject();
            int userId = -1;

            if (!StringUtils.isBlank(openId)) {Users user = userService.selectUserByOpenId(openId);
                if (user == null) {
                    // 新建一个用户信息
                    Users newUser = new Users();
                    newUser.setOpenid(openId);
                    newUser.setArea(city);
                    newUser.setSex(Integer.parseInt(gender));
                    newUser.setNickName(nickName);
                    newUser.setCreateTime(new Date());
                    newUser.setStatus(0);// 初始
                    if (!StringUtils.isBlank(unionid)) {newUser.setUnionid(unionid);
                    }
                    userService.instert(newUser);
                    userId = newUser.getId();} else {userId = user.getId();
                }
                json.put("userId", userId);
            }
            if (!StringUtils.isBlank(session_key) && !StringUtils.isBlank(openId)) {
               // 这段可不用 redis 存, 直接返回 session_key 也可以
                String userAgent = request.getHeader("user-agent");
                String sessionid = tokenService.generateToken(userAgent, session_key);
                // 将 session_key 存入 redis
                redisUtil.setex(sessionid, session_key + "###" + userId, Constants.SESSION_KEY_EX);
                json.put("token", sessionid);

                responseBean = new ResponseBean(true, json);
            } else {responseBean = new ResponseBean<>(false, null, errmsg);
            }
            return responseBean;
        } catch (Exception e) {e.printStackTrace();
            responseBean = new ResponseBean(false, UnicomResponseEnums.JSCODE2SESSION_ERRO);
            return responseBean;
        }
    }

解析:

这边我的登录获取的 session_key 出于安全性考虑没有直接在前端传输, 而是存到了 redis 里面给到前端 session_key 的 token 传输,
而且 session_key 的销毁时间是 20 分钟, 时间内可以重复获取用户数据.
如果只是简单使用或者对安全性要求不严的话可以直接传 session_key 到前端保存.

session_key 的作用:

** 校验用户信息 (wx.getUserInfo(OBJECT) 返回的 signature);
解密 (wx.getUserInfo(OBJECT) 返回的 encryptedData);**

按照官方的说法,wx.checksession 是用来检查 wx.login(OBJECT) 的时效性,判断登录是否过期;
疑惑的是(openid,unionid)都是用户唯一标识,不会因为 wx.login(OBJECT)的过期而改变,所以要是没有使用 wx.getUserInfo(OBJECT)获得的用户信息,确实没必要使用 wx.checksession()来检查 wx.login(OBJECT) 是否过期;
如果使用了 wx.getUserInfo(OBJECT)获得的用户信息,还是有必要使用 wx.checksession()来检查 wx.login(OBJECT) 是否过期的,因为用户有可能修改了头像、昵称、城市,省份等信息,可以通过检查 wx.login(OBJECT) 是否过期来更新着些信息;

小程序的登录状态维护本质就是维护 session_key 的时效性

这边附上我的 HttpUtils 工具代码, 如果只要用 get 的话可以复制部分:


/**
 * HttpUtils 工具类
 *
 * @author
 */
public class HttpUtils {

    /**
     * 请求方式:post
     */
    public static String POST = "post";

    /**
     * 编码格式:utf-8
     */
    private static final String CHARSET_UTF_8 = "UTF-8";

    /**
     * 报文头部 json
     */
    private static final String APPLICATION_JSON = "application/json";

    /**
     * 请求超时时间
     */
    private static final int CONNECT_TIMEOUT = 60 * 1000;

    /**
     * 传输超时时间
     */
    private static final int SO_TIMEOUT = 60 * 1000;

    /**
     * 日志
     */
    private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);

    /**
     * @param protocol
     * @param url
     * @param paraMap
     * @return
     * @throws Exception
     */
    public static String doPost(String protocol, String url,
                                Map<String, Object> paraMap) throws Exception {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse resp = null;
        String rtnValue = null;
        try {if (protocol.equals("http")) {httpClient = HttpClients.createDefault();
            } else {
                // 获取 https 安全客户端
                httpClient = HttpUtils.getHttpsClient();}

            HttpPost httpPost = new HttpPost(url);
            List<NameValuePair> list = msgs2valuePairs(paraMap);
//            List<NameValuePair> list = new ArrayList<NameValuePair>();
//            if (null != paraMap &&paraMap.size() > 0) {//                for (Entry<String, Object> entry : paraMap.entrySet()) {//                    list.add(new BasicNameValuePair(entry.getKey(), entry
//                            .getValue().toString()));
//                }
//            }

            RequestConfig requestConfig = RequestConfig.custom()
                    .setSocketTimeout(SO_TIMEOUT)
                    .setConnectTimeout(CONNECT_TIMEOUT).build();// 设置请求和传输超时时间
            httpPost.setConfig(requestConfig);
            httpPost.setEntity(new UrlEncodedFormEntity(list, CHARSET_UTF_8));
            resp = httpClient.execute(httpPost);
            rtnValue = EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);

        } catch (Exception e) {logger.error(e.getMessage());
            throw e;
        } finally {if (null != resp) {resp.close();
            }
            if (null != httpClient) {httpClient.close();
            }
        }

        return rtnValue;
    }

    /**
     * 获取 https,单向验证
     *
     * @return
     * @throws Exception
     */
    public static CloseableHttpClient getHttpsClient() throws Exception {
        try {TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate,
                        String paramString) throws CertificateException { }

                public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate,
                        String paramString) throws CertificateException { }

                public X509Certificate[] getAcceptedIssuers() {return null;}
            }};
            SSLContext sslContext = SSLContext
                    .getInstance(SSLConnectionSocketFactory.TLS);
            sslContext.init(new KeyManager[0], trustManagers,
                    new SecureRandom());
            SSLContext.setDefault(sslContext);
            sslContext.init(null, trustManagers, null);
            SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(
                    sslContext,
                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            HttpClientBuilder clientBuilder = HttpClients.custom()
                    .setSSLSocketFactory(connectionSocketFactory);
            clientBuilder.setRedirectStrategy(new LaxRedirectStrategy());
            CloseableHttpClient httpClient = clientBuilder.build();
            return httpClient;
        } catch (Exception e) {throw new Exception("http client 远程连接失败", e);
        }
    }

    /**
     * post 请求
     *
     * @param msgs
     * @param url
     * @return
     * @throws ClientProtocolException
     * @throws UnknownHostException
     * @throws IOException
     */
    public static String post(Map<String, Object> msgs, String url)
            throws ClientProtocolException, UnknownHostException, IOException {CloseableHttpClient httpClient = HttpClients.createDefault();
        try {HttpPost request = new HttpPost(url);
            List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
//            List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
//            if (null != msgs) {//                for (Entry<String, Object> entry : msgs.entrySet()) {//                    if (entry.getValue() != null) {//                        valuePairs.add(new BasicNameValuePair(entry.getKey(),
//                                entry.getValue().toString()));
//                    }
//                }
//            }
            request.setEntity(new UrlEncodedFormEntity(valuePairs, CHARSET_UTF_8));
            CloseableHttpResponse resp = httpClient.execute(request);
            return EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);
        } finally {httpClient.close();
        }
    }

    /**
     * post 请求
     *
     * @param msgs
     * @param url
     * @return
     * @throws ClientProtocolException
     * @throws UnknownHostException
     * @throws IOException
     */
    public static byte[] postGetByte(Map<String, Object> msgs, String url)
            throws ClientProtocolException, UnknownHostException, IOException {CloseableHttpClient httpClient = HttpClients.createDefault();
        InputStream inputStream = null;
        byte[] data = null;
        try {HttpPost request = new HttpPost(url);
            List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
//            List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
//            if (null != msgs) {//                for (Entry<String, Object> entry : msgs.entrySet()) {//                    if (entry.getValue() != null) {//                        valuePairs.add(new BasicNameValuePair(entry.getKey(),
//                                entry.getValue().toString()));
//                    }
//                }
//            }
            request.setEntity(new UrlEncodedFormEntity(valuePairs, CHARSET_UTF_8));
            CloseableHttpResponse response = httpClient.execute(request);
            try {
                // 获取相应实体
                HttpEntity entity = response.getEntity();
                if (entity != null) {inputStream = entity.getContent();
                    data = readInputStream(inputStream);
                }
                return data;
            } catch (Exception e) {e.printStackTrace();
            } finally {httpClient.close();
                return null;
            }
        } finally {httpClient.close();
        }
    }
    /**  将流 保存为数据数组
     * @param inStream
     * @return
     * @throws Exception
     */
    public static byte[] readInputStream(InputStream inStream) throws Exception {ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        // 创建一个 Buffer 字符串
        byte[] buffer = new byte[1024];
        // 每次读取的字符串长度,如果为 -1,代表全部读取完毕
        int len = 0;
        // 使用一个输入流从 buffer 里把数据读取出来
        while ((len = inStream.read(buffer)) != -1) {
            // 用输出流往 buffer 里写入数据,中间参数代表从哪个位置开始读,len 代表读取的长度
            outStream.write(buffer, 0, len);
        }
        // 关闭输入流
        inStream.close();
        // 把 outStream 里的数据写入内存
        return outStream.toByteArray();}

    /**
     * get 请求
     *
     * @param msgs
     * @param url
     * @return
     * @throws ClientProtocolException
     * @throws UnknownHostException
     * @throws IOException
     */
    public static String get(Map<String, Object> msgs, String url)
            throws ClientProtocolException, UnknownHostException, IOException {CloseableHttpClient httpClient = HttpClients.createDefault();
        try {List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
//            List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
//            if (null != msgs) {//                for (Entry<String, Object> entry : msgs.entrySet()) {//                    if (entry.getValue() != null) {//                        valuePairs.add(new BasicNameValuePair(entry.getKey(),
//                                entry.getValue().toString()));
//                    }
//                }
//            }
            // EntityUtils.toString(new UrlEncodedFormEntity(valuePairs),
            // CHARSET);
            url = url + "?" + URLEncodedUtils.format(valuePairs, CHARSET_UTF_8);
            HttpGet request = new HttpGet(url);
            CloseableHttpResponse resp = httpClient.execute(request);
            return EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);
        } finally {httpClient.close();
        }
    }

    public static <T> T post(Map<String, Object> msgs, String url,
                             Class<T> clazz) throws ClientProtocolException,
            UnknownHostException, IOException {String json = HttpUtils.post(msgs, url);
        T t = JSON.parseObject(json, clazz);
        return t;
    }

    public static <T> T get(Map<String, Object> msgs, String url, Class<T> clazz)
            throws ClientProtocolException, UnknownHostException, IOException {String json = HttpUtils.get(msgs, url);
        T t = JSON.parseObject(json, clazz);
        return t;
    }

    public static String postWithJson(Map<String, Object> msgs, String url)
            throws ClientProtocolException, IOException {CloseableHttpClient httpClient = HttpClients.createDefault();
        try {String jsonParam = JSON.toJSONString(msgs);

            HttpPost post = new HttpPost(url);
            post.setHeader("Content-Type", APPLICATION_JSON);
            post.setEntity(new StringEntity(jsonParam, CHARSET_UTF_8));
            CloseableHttpResponse response = httpClient.execute(post);

            return new String(EntityUtils.toString(response.getEntity()).getBytes("iso8859-1"), CHARSET_UTF_8);
        } finally {httpClient.close();
        }
    }

    public static <T> T postWithJson(Map<String, Object> msgs, String url, Class<T> clazz) throws ClientProtocolException,
            UnknownHostException, IOException {String json = HttpUtils.postWithJson(msgs, url);
        T t = JSON.parseObject(json, clazz);
        return t;
    }


    public static List<NameValuePair> msgs2valuePairs(Map<String, Object> msgs) {List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
        if (null != msgs) {for (Entry<String, Object> entry : msgs.entrySet()) {if (entry.getValue() != null) {valuePairs.add(new BasicNameValuePair(entry.getKey(),
                            entry.getValue().toString()));
                }
            }
        }
        return valuePairs;

    }

}

如果是直接传 session_key 到前端的, 下面的可以不用看了.
如果是 redis 存的 sesssion_key 的 token 的话, 这边附上登陆的时候的 token 转换为 session_key.

自定义拦截器注解:

import java.lang.annotation.*;


@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthIgnore {}

拦截器部分代码:



    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        AuthIgnore annotation;
        if(handler instanceof HandlerMethod) {annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthIgnore.class);
        }else{return true;}

        // 如果有 @AuthIgnore 注解,则不验证 token
        if(annotation != null){return true;}

//        // 获取微信 access_token;
//        if(!redisUtil.exists(Constants.ACCESS_TOKEN)){//            Map<String, Object> msgs = new HashMap<>();
//            msgs.put("appid",appId);
//            msgs.put("secret",secret);
//            msgs.put("grant_type","client_credential");
//            String data = HttpUtils.get(msgs,Constants.GETACCESSTOKEN); // java 的网络请求,返回字符串
//            String errcode= JSONObject.parseObject(data).getString("errcode");
//            String errmsg= JSONObject.parseObject(data).getString("errmsg");
//            if(StringUtils.isBlank(errcode)){
//                // 存储 access_token
//                String access_token= JSONObject.parseObject(data).getString("access_token");
//                long expires_in=Long.parseLong(JSONObject.parseObject(data).getString("expires_in"));
//                redisUtil.setex("ACCESS_TOKEN",access_token, expires_in);
//            }else{
//                // 异常返回数据拦截,返回 json 数据
//                response.setCharacterEncoding("UTF-8");
//                response.setContentType("application/json; charset=utf-8");
//                PrintWriter out = response.getWriter();
//                ResponseBean<Object> responseBean=new ResponseBean<>(false,null, errmsg);
//                out = response.getWriter();
//                out.append(JSON.toJSON(responseBean).toString());
//                return false;
//            }
//        }



       // 获取用户凭证
       String token = request.getHeader(Constants.USER_TOKEN);
//        if(StringUtils.isBlank(token)){//            token = request.getParameter(Constants.USER_TOKEN);
//        }
//        if(StringUtils.isBlank(token)){//            Object obj = request.getAttribute(Constants.USER_TOKEN);
//            if(null!=obj){//                token=obj.toString();
//            }
//        }
//        //token 凭证为空
//        if(StringUtils.isBlank(token)){
//            //token 不存在拦截,返回 json 数据
//            response.setCharacterEncoding("UTF-8");
//            response.setContentType("application/json; charset=utf-8");
//            PrintWriter out = response.getWriter();
//            try{//                ResponseBean<Object> responseBean=new ResponseBean<>(false,null, UnicomResponseEnums.TOKEN_EMPTY);
//                out = response.getWriter();
//                out.append(JSON.toJSON(responseBean).toString());
//                return false;
//            }
//            catch (Exception e) {//                e.printStackTrace();
//                response.sendError(500);
//                return false;
//            }
//        }

        if(token==null||!redisUtil.exists(token)){
            // 用户未登录,返回 json 数据
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = response.getWriter();
            try{ResponseBean<Object> responseBean=new ResponseBean<>(false,null, UnicomResponseEnums.DIS_LOGIN);
                out = response.getWriter();
                out.append(JSON.toJSON(responseBean).toString());
                return false;
            }
            catch (Exception e) {e.printStackTrace();
                response.sendError(500);
                return false;
            }
        }

        tokenManager.refreshUserToken(token);
        return true;
    }

}

过滤器配置:

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authorizationInterceptor())
                .addPathPatterns("/**")// 拦截所有请求,通过判断是否有 @AuthIgnore 注解 决定是否需要登录
                .excludePathPatterns("/user/login");// 排除登录
    }
    @Bean
    public AuthorizationInterceptor authorizationInterceptor() {return new AuthorizationInterceptor();
    }
}

token 管理:


@Service
public class TokenManager {

    @Resource
    private RedisUtil redisUtil;

    // 生成 token(格式为 token: 设备 - 加密的用户名 - 时间 - 六位随机数)
    public String generateToken(String userAgentStr, String username) {StringBuilder token = new StringBuilder("token:");
        // 设备
        UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
        if (userAgent.getOperatingSystem().isMobileDevice()) {token.append("MOBILE-");
        } else {token.append("PC-");
        }
        // 加密的用户名
        token.append(MD5Utils.MD5Encode(username) + "-");
        // 时间
        token.append(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "-");
        // 六位随机字符串
        token.append(UUID.randomUUID().toString());
        System.out.println("token-->" + token.toString());
        return token.toString();}


    /**
     * 登录用户,创建 token
     *
     * @param token
     */
    // 把 token 存到 redis 中
    public void save(String token, Users user) {if (token.startsWith("token:PC")) {redisUtil.setex(token,JSON.toJSONString(user), Constants.TOKEN_EX);
        } else {redisUtil.setex(token,JSON.toJSONString(user), Constants.TOKEN_EX);
        }
    }

    /**
     * 刷新用户
     *
     * @param token
     */
    public void refreshUserToken(String token) {if (redisUtil.exists(token)) {String value=redisUtil.get(token);
            redisUtil.setex(token, value, Constants.TOKEN_EX);

        }
    }

    /**
     * 用户退出登陆
     *
     * @param token
     */
    public void loginOut(String token) {redisUtil.remove(token);
    }


    /**
     * 获取用户信息
     *
     * @param token
     * @return
     */
    public Users getUserInfoByToken(String token) {if (redisUtil.exists(token)) {String jsonString = redisUtil.get(token);
            Users user =JSONObject.parseObject(jsonString, Users.class);
            return user;
        }
        return null;
    }



}

redis 工具类:


@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    public void set(String key, String value) {ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value);
    }

    public void setex(String key, String value, long seconds) {ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value, seconds,TimeUnit.SECONDS);

    }

    public Boolean exists(String key) {return redisTemplate.hasKey(key);
    }

    public void remove(String key) {redisTemplate.delete(key);
    }

    public String get(String key) {ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
}

最后 redis 要实现序列化, 序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是 IO,而我们的 IO 支持的数据格式就是字节数组。本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。



@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();

        // 使用 fastjson 序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // value 值的序列化采用 fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        // key 的序列化采用 StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}
退出移动版