原创

JustAuth升级到v1.8.1版本,新增AuthState工具类,可自动生成state

JustAuth(gitee | github),如你所见,它仅仅是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录SDK,让登录变得So easy!

JustAuth的功能

史上最全的整合第三方登录的工具,目前已支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软和今日头条等第三方平台的授权登录。 Login, so easy!

JustAuth特点

废话不多说,就俩字:

  1. :已集成十多家第三方平台(国内外常用的基本都已包含),后续依然还有扩展计划!
  2. :API就是奔着最简单去设计的(见后面快速开始),尽量让您用起来没有障碍感!

JustAuth(gitee | github)今日升级到1.8.1版本,新增了AuthState工具类,支持根据source自动生成state

AuthState.java

package me.zhyd.oauth.utils;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.request.ResponseStatus;

import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap;

/**
 * state工具,负责创建、获取和删除state
 *
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0
 * @since 1.8
 */
@Slf4j
public class AuthState {

    /**
     * 空字符串
     */
    private static final String EMPTY_STR = "";

    /**
     * state存储器
     */
    private static ConcurrentHashMap<String, String> stateBucket = new ConcurrentHashMap<>();

    /**
     * 生成随机的state
     *
     * @param source oauth平台
     * @return state
     */
    public static String create(String source) {
        return create(source, RandomUtil.randomString(4));
    }

    /**
     * 创建state
     *
     * @param source oauth平台
     * @param body   希望加密到state的消息体
     * @return state
     */
    public static String create(String source, Object body) {
        return create(source, JSON.toJSONString(body));
    }

    /**
     * 创建state
     *
     * @param source oauth平台
     * @param body   希望加密到state的消息体
     * @return state
     */
    public static String create(String source, String body) {
        String currentIp = getCurrentIp();
        String simpleKey = ((source + currentIp));
        String key = Base64.encode(simpleKey.getBytes(Charset.forName("UTF-8")));
        log.debug("Create the state: ip={}, platform={}, simpleKey={}, key={}, body={}", currentIp, source, simpleKey, key, body);

        if (stateBucket.containsKey(key)) {
            log.debug("Get from bucket: {}", stateBucket.get(key));
            return stateBucket.get(key);
        }

        String simpleState = source + "_" + currentIp + "_" + body;
        String state = Base64.encode(simpleState.getBytes(Charset.forName("UTF-8")));
        log.debug("Create a new state: {}", state, simpleState);
        stateBucket.put(key, state);
        return state;
    }

    /**
     * 获取state
     *
     * @param source oauth平台
     * @return state
     */
    public static String get(String source) {
        String currentIp = getCurrentIp();
        String simpleKey = ((source + currentIp));
        String key = Base64.encode(simpleKey.getBytes(Charset.forName("UTF-8")));
        log.debug("Get state by the key[{}], current ip[{}]", key, currentIp);
        return stateBucket.get(key);
    }

    /**
     * 获取state中保存的body内容
     *
     * @param source oauth平台
     * @param state  加密后的state
     * @param clazz  body的实际类型
     * @param <T>    需要转换的具体的class类型
     * @return state
     */
    public static <T> T getBody(String source, String state, Class<T> clazz) {
        if (StringUtils.isEmpty(state) || null == clazz) {
            return null;
        }
        log.debug("Get body from the state[{}] of the {} and convert it to {}", state, source, clazz.toString());
        String currentIp = getCurrentIp();
        String decodedState = Base64.decodeStr(state);
        log.debug("The decoded state is [{}]", decodedState);
        if (!decodedState.startsWith(source)) {
            return null;
        }
        String noneSourceState = decodedState.substring(source.length() + 1);
        if (!noneSourceState.startsWith(currentIp)) {
            // ip不相同,可能为非法的请求
            throw new AuthException(ResponseStatus.ILLEGAL_REQUEST);
        }
        String body = noneSourceState.substring(currentIp.length() + 1);
        log.debug("body is [{}]", body);
        if (clazz == String.class) {
            return (T) body;
        }
        if (clazz == Integer.class) {
            return (T) Integer.valueOf(Integer.parseInt(body));
        }
        if (clazz == Long.class) {
            return (T) Long.valueOf(Long.parseLong(body));
        }
        if (clazz == Short.class) {
            return (T) Short.valueOf(Short.parseShort(body));
        }
        if (clazz == Double.class) {
            return (T) Double.valueOf(Double.parseDouble(body));
        }
        if (clazz == Float.class) {
            return (T) Float.valueOf(Float.parseFloat(body));
        }
        if (clazz == Boolean.class) {
            return (T) Boolean.valueOf(Boolean.parseBoolean(body));
        }
        if (clazz == Byte.class) {
            return (T) Byte.valueOf(Byte.parseByte(body));
        }
        return JSON.parseObject(body, clazz);
    }

    /**
     * 登录成功后,清除state
     *
     * @param source oauth平台
     */
    public static void delete(String source) {
        String currentIp = getCurrentIp();

        String simpleKey = ((source + currentIp));
        String key = Base64.encode(simpleKey.getBytes(Charset.forName("UTF-8")));
        log.debug("Delete used state[{}] by the key[{}], current ip[{}]", stateBucket.get(key), key, currentIp);
        stateBucket.remove(key);
    }

    private static String getCurrentIp() {
        String currentIp = IpUtils.getIp();
        return StringUtils.isEmpty(currentIp) ? EMPTY_STR : currentIp;
    }
}

AuthState基本用法

工具类的基本用法如下:

@Test
public void usage() {
    String source = "github";
    System.out.println("\nstep1 生成state: 预期创建一个新的state...");
    String state = AuthState.create(source);
    System.out.println(state);

    System.out.println("\nstep2 重复生成state: 预期从bucket中返回一个可用的state...");
    String recreateState = AuthState.create(source);
    System.out.println(recreateState);
    Assert.assertEquals(state, recreateState);

    System.out.println("\nstep3 获取state: 预期获取上面生成的state...");
    String stateByBucket = AuthState.get(source);
    System.out.println(stateByBucket);
    Assert.assertEquals(state, stateByBucket);

    System.out.println("\nstep4 删除state: 预期删除掉上面创建的state...");
    AuthState.delete(source);

    System.out.println("\nstep5 重新获取state: 预期返回null...");
    String deletedState = AuthState.get(source);
    System.out.println(deletedState);
    Assert.assertNull(deletedState);
}

输出结果:

step1 生成state: 预期创建一个新的state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9lemV5

step2 重复生成state: 预期从bucket中返回一个可用的state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9lemV5

step3 获取state: 预期获取上面生成的state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9lemV5

step4 删除state: 预期删除掉上面创建的state...

step5 重新获取state: 预期返回null...
null

基础版的创建state的方法,适用于对state无复杂要求,只做简单验证时使用,内部通过source_ip_randomstr的方式生成state

AuthState扩展用法

到这一步,可能有朋友会觉得,这功能太鸡肋了,限制太大了,只能生成固定格式的随机字符串,没法利用state做一些复杂的业务操作。我只想说,“莫慌~~”,这才哪到哪。
如果需要对state做特殊处理,比如要在state中保存当前用户信息(id)、当前页面的标识(bind-oauth)等更多的特殊参数时,可以使用以下方法:

  • create(String source, String body)
    public static String create(String source, String body) {
      return xx;
    }
    
    这个方法接收一个string类型的变量, 可以传入复杂的字符串类型, 也可以自己封装数据后转成字符串,然后调用这个接口。当然,针对非字符串的特殊结构参数,可以使用下面的方法。
  • create(String source, Object body)
    public static String create(String source, Object body) {
      return create(source, JSON.toJSONString(body));
    }
    
    这个方法接收一个Object类型的变量,支持任意一种数据类型,可以传入map、list、实体类等复杂结构的参数。

接下来演示一下通过AuthState生成随机的、指定字符串的或者指定任意内容的state字符串。用法如下:

@Test
public void create() {
    String source = "github";
    System.out.println("\n通过随机字符串生成state...");
    String state = AuthState.create(source);
    System.out.println(state);
    AuthState.delete(source);

    System.out.println("\n通过传入自定义的字符串生成state...");
    String stringBody = "这是一个字符串";
    String stringState = AuthState.create(source, stringBody);
    System.out.println(stringState);
    AuthState.delete(source);

    System.out.println("\n通过传入数字生成state...");
    Integer numberBody = 111;
    String numberState = AuthState.create(source, numberBody);
    System.out.println(numberState);
    AuthState.delete(source);

    System.out.println("\n通过传入日期生成state...");
    Date dateBody = DateUtil.parse("2019-01-01 12:12:12", DatePattern.NORM_DATETIME_PATTERN);
    String dateState = AuthState.create(source, dateBody);
    System.out.println(dateState);
    AuthState.delete(source);

    System.out.println("\n通过传入map生成state...");
    Map<String, Object> mapBody = new HashMap<>();
    mapBody.put("userId", 1);
    mapBody.put("userToken", "xxxxx");
    String mapState = AuthState.create(source, mapBody);
    System.out.println(mapState);
    AuthState.delete(source);

    System.out.println("\n通过传入List生成state...");
    List<String> listBody = new ArrayList<>();
    listBody.add("xxxx");
    listBody.add("xxxxxxxx");
    String listState = AuthState.create(source, listBody);
    System.out.println(listState);
    AuthState.delete(source);

    System.out.println("\n通过传入实体类生成state...");
    AuthConfig entityBody = AuthConfig.builder()
            .clientId("xxxxx")
            .clientSecret("xxxxx")
            .build();
    String entityState = AuthState.create(source, entityBody);
    System.out.println(entityState);
    AuthState.delete(source);
}

输出结果:

通过随机字符串生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV81Y3pz

通过传入自定义的字符串生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV/ov5nmmK/kuIDkuKrlrZfnrKbkuLI=

通过传入数字生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV8xMTE=

通过传入日期生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV8xNTQ2MzE1OTMyMDAw

通过传入map生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV97InVzZXJUb2tlbiI6Inh4eHh4IiwidXNlcklkIjoxfQ==

通过传入List生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9bInh4eHgiLCJ4eHh4eHh4eCJd

通过传入实体类生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV97ImNsaWVudElkIjoieHh4eHgiLCJjbGllbnRTZWNyZXQiOiJ4eHh4eCIsInVuaW9uSWQiOmZhbHNlfQ==

怎么样? 是不是很爽?想往state中插入任何数据都可以。但是有些朋友可能又会说了:你这只生成了state,但是我怎么获取到我传入的具体的body内容?我就算生成的,但是我用不到这个数据,一样是“脱了裤子放屁 ——多此一举”。

咳~~ 还是那俩字:“莫慌~~”

获取自定的body

前面介绍到了, 可以通过create(String source, String body)或者create(String source, Object body)生成复杂类型的state,那么如何从encode之后的state中获取到定制的body内容呢?

AuthState提供了获取body内容的方法:

/**
 * 获取state中保存的body内容
 *
 * @param source oauth平台
 * @param state  加密后的state
 * @param clazz  body的实际类型
 * @param <T>    需要转换的具体的class类型
 * @return state
 */
public static <T> T getBody(String source, String state, Class<T> clazz) {
    ...
}

方法接收三个参数,参数的作用如上注释,只要是按照规定规则({source}_{ip}_{body})生成的state,都可以通过getBody方法解析出body内容。

示例代码:

@Test
public void getBody() {
    String source = "github";
    System.out.println("\n通过随机字符串生成state...");
    String state = AuthState.create(source);
    System.out.println(state);
    String body = AuthState.getBody(source, state, String.class);
    System.out.println(body);
    AuthState.delete(source);

    System.out.println("\n通过传入自定义的字符串生成state...");
    String stringBody = "这是一个字符串";
    String stringState = AuthState.create(source, stringBody);
    System.out.println(stringState);
    stringBody = AuthState.getBody(source, stringState, String.class);
    System.out.println(stringBody);
    AuthState.delete(source);

    System.out.println("\n通过传入数字生成state...");
    Integer numberBody = 111;
    String numberState = AuthState.create(source, numberBody);
    System.out.println(numberState);
    numberBody = AuthState.getBody(source, numberState, Integer.class);
    System.out.println(numberBody);
    AuthState.delete(source);

    System.out.println("\n通过传入日期生成state...");
    Date dateBody = DateUtil.parse("2019-01-01 12:12:12", DatePattern.NORM_DATETIME_PATTERN);
    String dateState = AuthState.create(source, dateBody);
    System.out.println(dateState);
    dateBody = AuthState.getBody(source, dateState, Date.class);
    System.out.println(dateBody);
    AuthState.delete(source);

    System.out.println("\n通过传入map生成state...");
    Map<String, Object> mapBody = new HashMap<>();
    mapBody.put("userId", 1);
    mapBody.put("userToken", "xxxxx");
    String mapState = AuthState.create(source, mapBody);
    System.out.println(mapState);
    mapBody = AuthState.getBody(source, mapState, Map.class);
    System.out.println(mapBody);
    AuthState.delete(source);

    System.out.println("\n通过传入List生成state...");
    List<String> listBody = new ArrayList<>();
    listBody.add("xxxx");
    listBody.add("xxxxxxxx");
    String listState = AuthState.create(source, listBody);
    System.out.println(listState);
    listBody = AuthState.getBody(source, listState, List.class);
    System.out.println(listBody);
    AuthState.delete(source);

    System.out.println("\n通过传入实体类生成state...");
    AuthConfig entityBody = AuthConfig.builder()
            .clientId("xxxxx")
            .clientSecret("xxxxx")
            .build();
    String entityState = AuthState.create(source, entityBody);
    System.out.println(entityState);
    entityBody = AuthState.getBody(source, entityState, AuthConfig.class);
    System.out.println(entityBody);
    AuthState.delete(source);
}

运行结果:

通过随机字符串生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9maXBo
fiph

通过传入自定义的字符串生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV/ov5nmmK/kuIDkuKrlrZfnrKbkuLI=
这是一个字符串

通过传入数字生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV8xMTE=
111

通过传入日期生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV8xNTQ2MzE1OTMyMDAw
Tue Jan 01 12:12:12 CST 2019

通过传入map生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV97InVzZXJUb2tlbiI6Inh4eHh4IiwidXNlcklkIjoxfQ==
{userToken=xxxxx, userId=1}

通过传入List生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV9bInh4eHgiLCJ4eHh4eHh4eCJd
[xxxx, xxxxxxxx]

通过传入实体类生成state...
Z2l0aHViXzE5Mi4xNjguMTkuMV97ImNsaWVudElkIjoieHh4eHgiLCJjbGllbnRTZWNyZXQiOiJ4eHh4eCIsInVuaW9uSWQiOmZhbHNlfQ==
me.zhyd.oauth.config.AuthConfig@725bef66

如上,通过getBody获取到具体的body数据后,调用方可以根据自己的规则去校验state或者做其他逻辑操作。

作者:咋样? 这回没有问题了吧?
刺头:切,你有能耐让代码按照我的意念生成任意格式的state?
作者:噗~~(一口老痰喷他一脸)

How to use?

前面主要通过源码+示例解释了AuthState的用法,那么我们在实际使用JustAuth时,只需要将state参数改为以下方式即可:

new AuthGithubRequest(AuthConfig.builder()
    .clientId("xx")
    .clientSecret("xx")
    .redirectUri("https://www.zhyd.me/oauth/callback/github")
    .state(AuthState.create(source)) // 1.8.1版本提供的生成state的方法 
    .build());

注意:授权登录后,需要手动清除本次请求流程中生成的state

AuthState.delete(source);

通过JustAuth-demo运行测试后的输出内容:

进入render:github
IP:192.168.19.1
2019-07-15 19:10:59 [me.zhyd.oauth.utils.AuthState:65] DEBUG - Create the state: ip=192.168.19.1, platform=github, simpleKey=github192.168.19.1, key=Z2l0aHViMTkyLjE2OC4xOS4x, body=n8gn
2019-07-15 19:10:59 [me.zhyd.oauth.utils.AuthState:74] DEBUG - Create a new state: Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu
https://github.com/login/oauth/authorize?client_id=xxx&amp;redirect_uri=http://dblog-web.zhyd.me/oauth/callback/github&amp;state=Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu
进入callback:github callback params:{"code":"609c1df58e66da9eb5c6","state":"Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu"}
2019-07-15 19:11:00 [me.zhyd.oauth.utils.AuthState:106] DEBUG - Get body from the state[Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu] of the github and convert it to class java.lang.String
2019-07-15 19:11:00 [me.zhyd.oauth.utils.AuthState:109] DEBUG - The decoded state is [github_192.168.19.1_n8gn]
2019-07-15 19:11:00 [me.zhyd.oauth.utils.AuthState:119] DEBUG - body is [n8gn]
获取state中的body信息:n8gn
IP:192.168.19.1
2019-07-15 19:11:00 [me.zhyd.oauth.utils.AuthState:65] DEBUG - Create the state: ip=192.168.19.1, platform=github, simpleKey=github192.168.19.1, key=Z2l0aHViMTkyLjE2OC4xOS4x, body=2s6v
2019-07-15 19:11:00 [me.zhyd.oauth.utils.AuthState:68] DEBUG - Get from bucket: Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu
{"code":2000,"data":{"avatar":"https://avatars3.githubusercontent.com/u/12689082?v=4","blog":"https://www.zhyd.me","company":"innodev","email":"yadong.zhang0415@gmail.com","gender":"UNKNOW","location":"Beijing","nickname":"yadong.zhang","remark":"心之所向,无所不能","source":"GITHUB","token":{"accessToken":"xx","expireIn":0},"username":"zhangyd-c","uuid":"xx"}}
2019-07-15 19:11:08 [me.zhyd.oauth.utils.AuthState:157] DEBUG - Delete used state[Z2l0aHViXzE5Mi4xNjguMTkuMV9uOGdu] by the key[Z2l0aHViMTkyLjE2OC4xOS4x], current ip[192.168.19.1]

项目源码

相关文章

其他开源作品

  • blog-hunter,一款简单好用并且支持多个平台的博客爬取工具
  • OneBlog,一个简洁美观、功能强大并且自适应的Java博客
  • JustAuth,史上最全的整合第三方登录的工具,目前已支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软和今日头条等第三方平台的授权登录。Login, so easy!
  • spingboot-shiro,Springboot + shiro权限管理。这或许是流程最详细、代码最干净、配置最简单的shiro上手项目了。
  • braum-spring-boot-starter,Braum可以很方便的帮助开发人员过滤、识别恶意请求
正文到此结束