基于redis的小程序登录实现方法流程分析

这张图是小程序的登录流程解析:

基于redis的小程序登录实现方法流程分析_第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' //这个是固定的

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

基于redis的小程序登录实现方法流程分析_第2张图片

详情可以看下官方文档: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 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 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 list = msgs2valuePairs(paraMap);
// List list = new ArrayList();
// if (null != paraMap &¶Map.size() > 0) {
// for (Entry 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 msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 try {
 HttpPost request = new HttpPost(url);
 List valuePairs = msgs2valuePairs(msgs);
// List valuePairs = new ArrayList();
// if (null != msgs) {
// for (Entry 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 msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 InputStream inputStream = null;
 byte[] data = null;
 try {
 HttpPost request = new HttpPost(url);
 List valuePairs = msgs2valuePairs(msgs);
// List valuePairs = new ArrayList();
// if (null != msgs) {
// for (Entry 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 msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 try {
 List valuePairs = msgs2valuePairs(msgs);
// List valuePairs = new ArrayList();
// if (null != msgs) {
// for (Entry 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 post(Map msgs, String url,
  Class clazz) throws ClientProtocolException,
 UnknownHostException, IOException {
 String json = HttpUtils.post(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static  T get(Map msgs, String url, Class clazz)
 throws ClientProtocolException, UnknownHostException, IOException {
 String json = HttpUtils.get(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static String postWithJson(Map 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 postWithJson(Map msgs, String url, Class clazz) throws ClientProtocolException,
 UnknownHostException, IOException {
 String json = HttpUtils.postWithJson(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static List msgs2valuePairs(Map msgs) {
 List valuePairs = new ArrayList();
 if (null != msgs) {
 for (Entry 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 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 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 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 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 redisTemplate;
 public void set(String key, String value) {
 ValueOperations valueOperations = redisTemplate.opsForValue();
 valueOperations.set(key, value);
 }
 public void setex(String key, String value, long seconds) {
 ValueOperations 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 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 redisTemplate(
 RedisConnectionFactory redisConnectionFactory) {
 RedisTemplate 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;
 }
}

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

总结

到此这篇关于基于redis的小程序登录实现的文章就介绍到这了,更多相关redis小程序登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(基于redis的小程序登录实现方法流程分析)