map) {
+ for (String key : map.keySet()) {
+ this.put(key, map.get(key));
+ }
+ return this;
+ }
+
+
+ // ============================ 构建 ==================================
+
+ public AjaxJson(int code, String msg, Object data, Long dataCount) {
+ this.setCode(code);
+ this.setMsg(msg);
+ this.setData(data);
+ this.setDataCount(dataCount == null ? -1 : dataCount);
+ }
+
+ /** 返回成功 */
+ public static AjaxJson getSuccess() {
+ return new AjaxJson(CODE_SUCCESS, "ok", null, null);
+ }
+ public static AjaxJson getSuccess(String msg) {
+ return new AjaxJson(CODE_SUCCESS, msg, null, null);
+ }
+ public static AjaxJson getSuccess(String msg, Object data) {
+ return new AjaxJson(CODE_SUCCESS, msg, data, null);
+ }
+ public static AjaxJson getSuccessData(Object data) {
+ return new AjaxJson(CODE_SUCCESS, "ok", data, null);
+ }
+
+
+ /** 返回失败 */
+ public static AjaxJson getError() {
+ return new AjaxJson(CODE_ERROR, "error", null, null);
+ }
+ public static AjaxJson getError(String msg) {
+ return new AjaxJson(CODE_ERROR, msg, null, null);
+ }
+
+ /** 返回警告 */
+ public static AjaxJson getWarning() {
+ return new AjaxJson(CODE_ERROR, "warning", null, null);
+ }
+ public static AjaxJson getWarning(String msg) {
+ return new AjaxJson(CODE_WARNING, msg, null, null);
+ }
+
+ /** 返回未登录 */
+ public static AjaxJson getNotLogin() {
+ return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
+ }
+
+ /** 返回没有权限的 */
+ public static AjaxJson getNotJur(String msg) {
+ return new AjaxJson(CODE_NOT_JUR, msg, null, null);
+ }
+
+ /** 返回一个自定义状态码的 */
+ public static AjaxJson get(int code, String msg){
+ return new AjaxJson(code, msg, null, null);
+ }
+
+ /** 返回分页和数据的 */
+ public static AjaxJson getPageData(Long dataCount, Object data){
+ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
+ }
+
+ /** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
+ public static AjaxJson getByLine(int line){
+ if(line > 0){
+ return getSuccess("ok", line);
+ }
+ return getError("error").setData(line);
+ }
+
+ /** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */
+ public static AjaxJson getByBoolean(boolean b){
+ return b ? getSuccess("ok") : getError("error");
+ }
+
+
+
+
+
+
+
+// // 历史版本遗留代码
+// public int code; // 状态码
+// public String msg; // 描述信息
+// public Object data; // 携带对象
+// public Long dataCount; // 数据总数,用于分页
+
+
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java
new file mode 100644
index 00000000..e4524216
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java
@@ -0,0 +1,723 @@
+package com.pj.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦
+ * 所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用
+ *
最新:2020-12-10 新增部分构造方法
+ * @author kong
+ */
+public class SoMap extends LinkedHashMap {
+
+ private static final long serialVersionUID = 1L;
+
+ public SoMap() {
+ }
+
+
+ /** 以下元素会在isNull函数中被判定为Null, */
+ public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
+ public static final List NULL_ELEMENT_LIST;
+
+
+ static {
+ NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
+ }
+
+ // ============================= 读值 =============================
+
+ /** 获取一个值 */
+ @Override
+ public Object get(Object key) {
+ if("this".equals(key)) {
+ return this;
+ }
+ return super.get(key);
+ }
+
+ /** 如果为空,则返回默认值 */
+ public Object get(Object key, Object defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ /** 转为String并返回 */
+ public String getString(String key) {
+ Object value = get(key);
+ if(value == null) {
+ return null;
+ }
+ return String.valueOf(value);
+ }
+
+ /** 如果为空,则返回默认值 */
+ public String getString(String key, String defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return String.valueOf(value);
+ }
+
+ /** 转为int并返回 */
+ public int getInt(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0;
+ }
+ return Integer.valueOf(String.valueOf(value));
+ }
+ /** 转为int并返回,同时指定默认值 */
+ public int getInt(String key, int defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return Integer.valueOf(String.valueOf(value));
+ }
+
+ /** 转为long并返回 */
+ public long getLong(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0;
+ }
+ return Long.valueOf(String.valueOf(value));
+ }
+
+ /** 转为double并返回 */
+ public double getDouble(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0.0;
+ }
+ return Double.valueOf(String.valueOf(value));
+ }
+
+ /** 转为boolean并返回 */
+ public boolean getBoolean(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return false;
+ }
+ return Boolean.valueOf(String.valueOf(value));
+ }
+
+ /** 转为Date并返回,根据自定义格式 */
+ public Date getDateByFormat(String key, String format) {
+ try {
+ return new SimpleDateFormat(format).parse(getString(key));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** 转为Date并返回,根据格式: yyyy-MM-dd */
+ public Date getDate(String key) {
+ return getDateByFormat(key, "yyyy-MM-dd");
+ }
+
+ /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */
+ public Date getDateTime(String key) {
+ return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
+ }
+
+ /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
+ @SuppressWarnings("unchecked")
+ public List getList(String key) {
+ Object value = get(key);
+ List list = null;
+ if(value == null || value.equals("")) {
+ list = new ArrayList();
+ }
+ else if(value instanceof List) {
+ list = (List)value;
+ } else {
+ list = new ArrayList();
+ list.add(value);
+ }
+ return list;
+ }
+
+ /** 获取集合 (指定泛型类型) */
+ public List getList(String key, Class cs) {
+ List list = getList(key);
+ List list2 = new ArrayList();
+ for (Object obj : list) {
+ T objC = getValueByClass(obj, cs);
+ list2.add(objC);
+ }
+ return list2;
+ }
+
+ /** 获取集合(逗号分隔式),(指定类型) */
+ public List getListByComma(String key, Class cs) {
+ String listStr = getString(key);
+ if(listStr == null || listStr.equals("")) {
+ return new ArrayList<>();
+ }
+ // 开始转化
+ String [] arr = listStr.split(",");
+ List list = new ArrayList();
+ for (String str : arr) {
+ if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {
+ str = str.trim();
+ }
+ T objC = getValueByClass(str, cs);
+ list.add(objC);
+ }
+ return list;
+ }
+
+
+ /** 根据指定类型从map中取值,返回实体对象 */
+ public T getModel(Class cs) {
+ try {
+ return getModelByObject(cs.newInstance());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** 从map中取值,塞到一个对象中 */
+ public T getModelByObject(T obj) {
+ // 获取类型
+ Class> cs = obj.getClass();
+ // 循环复制
+ for (Field field : cs.getDeclaredFields()) {
+ try {
+ // 获取对象
+ Object value = this.get(field.getName());
+ if(value == null) {
+ continue;
+ }
+ field.setAccessible(true);
+ Object valueConvert = getValueByClass(value, field.getType());
+ field.set(obj, valueConvert);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException("属性取值出错:" + field.getName(), e);
+ }
+ }
+ return obj;
+ }
+
+
+
+ /**
+ * 将指定值转化为指定类型并返回
+ * @param obj
+ * @param cs
+ * @param
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static T getValueByClass(Object obj, Class cs) {
+ String obj2 = String.valueOf(obj);
+ Object obj3 = null;
+ if (cs.equals(String.class)) {
+ obj3 = obj2;
+ } else if (cs.equals(int.class) || cs.equals(Integer.class)) {
+ obj3 = Integer.valueOf(obj2);
+ } else if (cs.equals(long.class) || cs.equals(Long.class)) {
+ obj3 = Long.valueOf(obj2);
+ } else if (cs.equals(short.class) || cs.equals(Short.class)) {
+ obj3 = Short.valueOf(obj2);
+ } else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
+ obj3 = Byte.valueOf(obj2);
+ } else if (cs.equals(float.class) || cs.equals(Float.class)) {
+ obj3 = Float.valueOf(obj2);
+ } else if (cs.equals(double.class) || cs.equals(Double.class)) {
+ obj3 = Double.valueOf(obj2);
+ } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
+ obj3 = Boolean.valueOf(obj2);
+ } else {
+ obj3 = (T)obj;
+ }
+ return (T)obj3;
+ }
+
+
+ // ============================= 写值 =============================
+
+ /**
+ * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去)
+ */
+ public void setDefaultValue(String key, Object defaultValue) {
+ if(isNull(key)) {
+ set(key, defaultValue);
+ }
+ }
+
+ /** set一个值,连缀风格 */
+ public SoMap set(String key, Object value) {
+ // 防止敏感key
+ if(key.toLowerCase().equals("this")) {
+ return this;
+ }
+ put(key, value);
+ return this;
+ }
+
+ /** 将一个Map塞进SoMap */
+ public SoMap setMap(Map map) {
+ if(map != null) {
+ for (String key : map.keySet()) {
+ this.set(key, map.get(key));
+ }
+ }
+ return this;
+ }
+
+ /** 将一个对象解析塞进SoMap */
+ public SoMap setModel(Object model) {
+ if(model == null) {
+ return this;
+ }
+ Field[] fields = model.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ try{
+ field.setAccessible(true);
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ if(!isStatic) {
+ this.set(field.getName(), field.get(model));
+ }
+ }catch (Exception e){
+ throw new RuntimeException(e);
+ }
+ }
+ return this;
+ }
+
+ /** 将json字符串解析后塞进SoMap */
+ public SoMap setJsonString(String jsonString) {
+ try {
+ @SuppressWarnings("unchecked")
+ Map map = new ObjectMapper().readValue(jsonString, Map.class);
+ return this.setMap(map);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ // ============================= 删值 =============================
+
+ /** delete一个值,连缀风格 */
+ public SoMap delete(String key) {
+ remove(key);
+ return this;
+ }
+
+ /** 清理所有value为null的字段 */
+ public SoMap clearNull() {
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(this.isNull(key)) {
+ iterator.remove();
+ this.remove(key);
+ }
+
+ }
+ return this;
+ }
+ /** 清理指定key */
+ public SoMap clearIn(String ...keys) {
+ List keys2 = Arrays.asList(keys);
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(keys2.contains(key) == true) {
+ iterator.remove();
+ this.remove(key);
+ }
+ }
+ return this;
+ }
+ /** 清理掉不在列表中的key */
+ public SoMap clearNotIn(String ...keys) {
+ List keys2 = Arrays.asList(keys);
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(keys2.contains(key) == false) {
+ iterator.remove();
+ this.remove(key);
+ }
+
+ }
+ return this;
+ }
+ /** 清理掉所有key */
+ public SoMap clearAll() {
+ clear();
+ return this;
+ }
+
+
+ // ============================= 快速构建 =============================
+
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap() {
+ return new SoMap();
+ }
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap(String key, Object value) {
+ return new SoMap().set(key, value);
+ }
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap(Map map) {
+ return new SoMap().setMap(map);
+ }
+
+ /** 将一个对象集合解析成为SoMap */
+ public static SoMap getSoMapByModel(Object model) {
+ return SoMap.getSoMap().setModel(model);
+ }
+
+ /** 将一个对象集合解析成为SoMap集合 */
+ public static List getSoMapByList(List> list) {
+ List listMap = new ArrayList();
+ for (Object model : list) {
+ listMap.add(getSoMapByModel(model));
+ }
+ return listMap;
+ }
+
+ /** 克隆指定key,返回一个新的SoMap */
+ public SoMap cloneKeys(String... keys) {
+ SoMap so = new SoMap();
+ for (String key : keys) {
+ so.set(key, this.get(key));
+ }
+ return so;
+ }
+ /** 克隆所有key,返回一个新的SoMap */
+ public SoMap cloneSoMap() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key, this.get(key));
+ }
+ return so;
+ }
+
+ /** 将所有key转为大写 */
+ public SoMap toUpperCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key.toUpperCase(), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key转为小写 */
+ public SoMap toLowerCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key.toLowerCase(), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中下划线转为中划线模式 (kebab-case风格) */
+ public SoMap toKebabCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordEachKebabCase(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中下划线转为小驼峰模式 */
+ public SoMap toHumpCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordEachBigFs(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中小驼峰转为下划线模式 */
+ public SoMap humpToLineCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordHumpToLine(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+
+
+
+
+ // ============================= 辅助方法 =============================
+
+
+ /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */
+ public boolean isNull(String key) {
+ return valueIsNull(get(key));
+ }
+
+ /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */
+ public boolean isContainNull(String ...keys) {
+ for (String key : keys) {
+ if(this.isNull(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** 与isNull()相反 */
+ public boolean isNotNull(String key) {
+ return !isNull(key);
+ }
+ /** 指定key的value是否为null,作用同isNotNull() */
+ public boolean has(String key) {
+ return !isNull(key);
+ }
+
+ /** 指定value在此SoMap的判断标准中是否为null */
+ public boolean valueIsNull(Object value) {
+ return NULL_ELEMENT_LIST.contains(value);
+ }
+
+ /** 验证指定key不为空,为空则抛出异常 */
+ public SoMap checkNull(String ...keys) {
+ for (String key : keys) {
+ if(this.isNull(key)) {
+ throw new RuntimeException("参数" + key + "不能为空");
+ }
+ }
+ return this;
+ }
+
+ static Pattern patternNumber = Pattern.compile("[0-9]*");
+ /** 指定key是否为数字 */
+ public boolean isNumber(String key) {
+ String value = getString(key);
+ if(value == null) {
+ return false;
+ }
+ return patternNumber.matcher(value).matches();
+ }
+
+
+
+
+ /**
+ * 转为JSON字符串
+ */
+ public String toJsonString() {
+ try {
+// SoMap so = SoMap.getSoMap(this);
+ return new ObjectMapper().writeValueAsString(this);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+// /**
+// * 转为JSON字符串, 带格式的
+// */
+// public String toJsonFormatString() {
+// try {
+// return JSON.toJSONString(this, true);
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+// }
+
+ // ============================= web辅助 =============================
+
+
+ /**
+ * 返回当前request请求的的所有参数
+ * @return
+ */
+ public static SoMap getRequestSoMap() {
+ // 大善人SpringMVC提供的封装
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if(servletRequestAttributes == null) {
+ throw new RuntimeException("当前线程非JavaWeb环境");
+ }
+ // 当前request
+ HttpServletRequest request = servletRequestAttributes.getRequest();
+ if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) {
+ initRequestSoMap(request);
+ }
+ return (SoMap)request.getAttribute("currentSoMap");
+ }
+
+ /** 初始化当前request的 SoMap */
+ private static void initRequestSoMap(HttpServletRequest request) {
+ SoMap soMap = new SoMap();
+ Map parameterMap = request.getParameterMap(); // 获取所有参数
+ for (String key : parameterMap.keySet()) {
+ try {
+ String[] values = parameterMap.get(key); // 获得values
+ if(values.length == 1) {
+ soMap.set(key, values[0]);
+ } else {
+ List list = new ArrayList();
+ for (String v : values) {
+ list.add(v);
+ }
+ soMap.set(key, list);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ request.setAttribute("currentSoMap", soMap);
+ }
+
+ /**
+ * 验证返回当前线程是否为JavaWeb环境
+ * @return
+ */
+ public static boolean isJavaWeb() {
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装
+ if(servletRequestAttributes == null) {
+ return false;
+ }
+ return true;
+ }
+
+
+
+ // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) =============================
+
+ /** get 当前页 */
+ public int getKeyPageNo() {
+ int pageNo = getInt("pageNo", 1);
+ if(pageNo <= 0) {
+ pageNo = 1;
+ }
+ return pageNo;
+ }
+ /** get 页大小 */
+ public int getKeyPageSize() {
+ int pageSize = getInt("pageSize", 10);
+ if(pageSize <= 0 || pageSize > 1000) {
+ pageSize = 10;
+ }
+ return pageSize;
+ }
+
+ /** get 排序方式 */
+ public int getKeySortType() {
+ return getInt("sortType");
+ }
+
+
+
+
+
+
+ // ============================= 工具方法 =============================
+
+
+ /**
+ * 将一个一维集合转换为树形集合
+ * @param list 集合
+ * @param idKey id标识key
+ * @param parentIdKey 父id标识key
+ * @param childListKey 子节点标识key
+ * @return 转换后的tree集合
+ */
+ public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) {
+ // 声明新的集合,存储tree形数据
+ List newTreeList = new ArrayList();
+ // 声明hash-Map,方便查找数据
+ SoMap hash = new SoMap();
+ // 将数组转为Object的形式,key为数组中的id
+ for (int i = 0; i < list.size(); i++) {
+ SoMap json = (SoMap) list.get(i);
+ hash.put(json.getString(idKey), json);
+ }
+ // 遍历结果集
+ for (int j = 0; j < list.size(); j++) {
+ // 单条记录
+ SoMap aVal = (SoMap) list.get(j);
+ // 在hash中取出key为单条记录中pid的值
+ SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString());
+ // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
+ if (hashVp != null) {
+ // 检查是否有child属性,有则添加,没有则新建
+ if (hashVp.get(childListKey) != null) {
+ @SuppressWarnings("unchecked")
+ List ch = (List) hashVp.get(childListKey);
+ ch.add(aVal);
+ hashVp.put(childListKey, ch);
+ } else {
+ List ch = new ArrayList();
+ ch.add(aVal);
+ hashVp.put(childListKey, ch);
+ }
+ } else {
+ newTreeList.add(aVal);
+ }
+ }
+ return newTreeList;
+ }
+
+
+
+ /** 指定字符串的字符串下划线转大写模式 */
+ private static String wordEachBig(String str){
+ String newStr = "";
+ for (String s : str.split("_")) {
+ newStr += wordFirstBig(s);
+ }
+ return newStr;
+ }
+ /** 返回下划线转小驼峰形式 */
+ private static String wordEachBigFs(String str){
+ return wordFirstSmall(wordEachBig(str));
+ }
+
+ /** 将指定单词首字母大写 */
+ private static String wordFirstBig(String str) {
+ return str.substring(0, 1).toUpperCase() + str.substring(1, str.length());
+ }
+
+ /** 将指定单词首字母小写 */
+ private static String wordFirstSmall(String str) {
+ return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
+ }
+
+ /** 下划线转中划线 */
+ private static String wordEachKebabCase(String str) {
+ return str.replaceAll("_", "-");
+ }
+
+ /** 驼峰转下划线 */
+ private static String wordHumpToLine(String str) {
+ return str.replaceAll("[A-Z]", "_$0").toLowerCase();
+ }
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml
new file mode 100644
index 00000000..5eac1b0d
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml
@@ -0,0 +1,14 @@
+server:
+ port: 8002
+
+spring:
+ # 静态文件路径映射
+ resources:
+ static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
+ # static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-client\src\main\resources\static\
+
+ # sa-token配置
+ sa-token:
+ # token名称 (同时也是cookie名称)
+ token-name: satoken-client
+
\ No newline at end of file
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/static/login.html b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/static/login.html
new file mode 100644
index 00000000..0287668a
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/static/login.html
@@ -0,0 +1,127 @@
+
+
+
+
+ 客户端-登录页
+
+
+
+
+
客户端-登录页
+
+ 当前是否登录:
+ 注销登录
+
+
+ 点此按钮开始使用OAuth2.0开放平台快捷登录:
+
快捷登录
+
+
+
+
+
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/.gitignore b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/.gitignore
new file mode 100644
index 00000000..304e8d54
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/.gitignore
@@ -0,0 +1,13 @@
+target/
+.project
+.classpath
+.settings
+
+/.idea/
+
+node_modules/
+bin/
+.settings/
+unpackage/
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml
new file mode 100644
index 00000000..be445b4b
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml
@@ -0,0 +1,66 @@
+
+ 4.0.0
+ com.pj
+ sa-token-demo-oauth2-server
+ 0.0.1-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.3.RELEASE
+
+
+
+
+ 1.8
+ 3.1.1
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.13.0
+
+
+
+
+ cn.dev33
+ sa-token-oauth2
+ 1.13.0-alpha
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java
new file mode 100644
index 00000000..00eccc07
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java
@@ -0,0 +1,18 @@
+package com.pj;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * 启动
+ * @author kong
+ */
+@SpringBootApplication
+public class SaOAuth2ServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SaOAuth2ServerApplication.class, args);
+ System.out.println("\n服务端启动成功");
+ }
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ExceptionHandle.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ExceptionHandle.java
new file mode 100644
index 00000000..ed687f52
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ExceptionHandle.java
@@ -0,0 +1,59 @@
+package com.pj.controller;
+
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.pj.utils.AjaxJson;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.exception.NotRoleException;
+
+/**
+ * 全局异常拦截
+ * @ControllerAdvice 可指定包前缀,例如:(basePackages = "com.pj.controller.admin")
+ * @author kong
+ *
+ */
+@ControllerAdvice
+public class ExceptionHandle {
+
+
+ /** 全局异常拦截 */
+ @ResponseBody
+ @ExceptionHandler
+ public AjaxJson handlerException(Exception e) {
+
+ // 打印堆栈,以供调试
+ e.printStackTrace();
+
+ // 记录日志信息
+ AjaxJson aj = null;
+
+ // ------------- 判断异常类型,提供个性化提示信息
+
+ // 如果是未登录异常
+ if(e instanceof NotLoginException){
+ aj = AjaxJson.getNotLogin();
+ }
+ // 如果是角色异常
+ else if(e instanceof NotRoleException) {
+ NotPermissionException ee = (NotPermissionException) e;
+ aj = AjaxJson.getNotJur("无此角色:" + ee.getCode());
+ }
+ // 如果是权限异常
+ else if(e instanceof NotPermissionException) {
+ NotPermissionException ee = (NotPermissionException) e;
+ aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
+ }
+ // 普通异常输出:500 + 异常信息
+ else {
+ aj = AjaxJson.getError(e.getMessage());
+ }
+
+ // 返回到前台
+ return aj;
+ }
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/OAuth2Controller.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/OAuth2Controller.java
new file mode 100644
index 00000000..9d8d6957
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/OAuth2Controller.java
@@ -0,0 +1,147 @@
+package com.pj.controller;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.pj.utils.AjaxJson;
+import com.pj.utils.SoMap;
+
+import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
+import cn.dev33.satoken.oauth2.model.AccessTokenModel;
+import cn.dev33.satoken.oauth2.model.CodeModel;
+import cn.dev33.satoken.oauth2.model.RequestAuthModel;
+import cn.dev33.satoken.stp.StpUtil;
+
+@RestController
+@RequestMapping("/oauth2/")
+public class OAuth2Controller {
+
+
+ // 获取授权码
+ @RequestMapping("/authorize")
+ public AjaxJson authorize(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ // 获取参数
+ System.out.println("------------------ 成功进入请求 ------------------");
+
+ // 如果暂未登录,则先跳转到登录页 (转发)
+ if(StpUtil.isLogin() == false) {
+ response.setContentType("text/html");
+ request.getRequestDispatcher("/login.html").forward(request, response);
+ return AjaxJson.getSuccess();
+ }
+
+ // 构建Model
+ RequestAuthModel authModel = new RequestAuthModel()
+ .setClientId(request.getParameter("client_id")) // 应用id
+ .setScope(request.getParameter("scope")) // 授权类型
+ .setLoginId(StpUtil.getLoginIdAsLong()) // 当前登录账号id
+ .setRedirectUri(URLDecoder.decode(request.getParameter("redirect_uri"), "utf-8")) // 重定向地址
+ .setResponseType(request.getParameter("response_type")) // 返回类型
+ .setState(request.getParameter("state")) // 状态值
+ .checkModel(); // 校验参数完整性
+
+ // 生成授权码Model
+ CodeModel codeModel = SaOAuth2Util.generateCode(authModel);
+
+ // 打印调试
+ System.out.println("应用id=" + authModel.getClientId() + "请求授权,授权类型=" + authModel.getResponseType());
+ System.out.println("重定向地址:" + authModel.getRedirectUri());
+ System.out.println("拼接完成的redirect_uri: " + codeModel.getRedirectUri());
+ System.out.println("如果用户拒绝授权,则重定向至: " + codeModel.getRejectUri());
+
+ // 如果请求的权限用户已经确认,直接开始重定向授权
+ if(codeModel.getIsConfirm() == true) {
+ response.sendRedirect(codeModel.getRedirectUri());
+ } else {
+ // 如果请求的权限用户尚未确认,则进入到确定页
+ request.setAttribute("name", "sdd");
+ response.sendRedirect("/auth.html?code=" + codeModel.getCode());
+ }
+
+ return AjaxJson.getSuccess();
+ }
+
+ // 根据授权码获取应用信息
+ @RequestMapping("/getCodeInfo")
+ public AjaxJson getCodeInfo(String code) {
+ // 获取codeModel
+ CodeModel codeModel = SaOAuth2Util.getCode(code);
+ System.out.println(code);
+ System.out.println(codeModel);
+ // 返回
+ return AjaxJson.getSuccessData(codeModel);
+ }
+
+ // 确认授权一个授权码
+ @RequestMapping("/confirm")
+ public AjaxJson confirm(String code) {
+ // 获取codeModel
+ CodeModel codeModel = SaOAuth2Util.getCode(code);
+ if(codeModel == null) {
+ return AjaxJson.getError("无效code码");
+ }
+ // 此处的判断是为了保证当前账号id 和 创建授权码的账号id一致 才可以进行确认
+ if(codeModel.getLoginId().toString().equals(StpUtil.getLoginIdAsString()) == false) {
+ return AjaxJson.getError("暂无权限");
+ }
+ // 进行确认
+ SaOAuth2Util.confirmCode(code);
+
+ // 返回ok
+ return AjaxJson.getSuccess();
+ }
+
+ // 根据授权码等参数,获取 access_token 等信息
+ @RequestMapping("/getAccessToken")
+ public SoMap getAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ // 获取参数
+ System.out.println("------------------ 成功进入请求 ------------------");
+ String code = request.getParameter("code"); // code码
+ String clientId = request.getParameter("client_id"); // 应用id
+ String clientSecret = request.getParameter("client_secret"); // 应用秘钥
+
+ // 校验参数
+ SaOAuth2Util.checkCodeIdSecret(code, clientId, clientSecret);
+
+ // 生成
+ CodeModel codeModel = SaOAuth2Util.getCode(code);
+ AccessTokenModel tokenModel = SaOAuth2Util.generateAccessToken(codeModel);
+
+ // 生成AccessToken之后,将授权码立即销毁
+ SaOAuth2Util.deleteCode(code);
+
+ // 返回
+ return SoMap.getSoMap()
+ .setModel(tokenModel)
+ .set("code", 200)
+ .set("msg", "ok");
+ }
+
+ // 根据access_token返回指定的资源
+ @RequestMapping("/getResources")
+ public SoMap getResources(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+ // 获取信息
+ String accessToken = request.getParameter("access_token");
+ Object LoginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
+ System.out.println("LoginId=" + LoginId);
+
+ // 根据LoginId获取相应信息...
+ // 此处仅做模拟
+ return new SoMap()
+ .set("nickname", "shengzhang")
+ .set("acatar", "xxx")
+ .set("sex", 1);
+ }
+
+
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ServerAccController.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ServerAccController.java
new file mode 100644
index 00000000..1c888709
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/controller/ServerAccController.java
@@ -0,0 +1,28 @@
+package com.pj.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.pj.utils.AjaxJson;
+
+import cn.dev33.satoken.stp.StpUtil;
+
+/**
+ * 服务端登录Controller
+ * @author kong
+ */
+@RestController
+public class ServerAccController {
+
+ // 登录方法
+ @RequestMapping("/doLogin")
+ public AjaxJson test(String username, String password) {
+ System.out.println("------------------ 成功进入请求 ------------------");
+ if("test".equals(username) && "test".equals(password)) {
+ StpUtil.setLoginId(10001);
+ return AjaxJson.getSuccess();
+ }
+ return AjaxJson.getError();
+ }
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2InterfaceImpl.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2InterfaceImpl.java
new file mode 100644
index 00000000..708c73fa
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2InterfaceImpl.java
@@ -0,0 +1,72 @@
+package com.pj.oauth2;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+
+import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
+
+/**
+ * 使用oauth2.0 所必须的一些自定义实现
+ * @author kong
+ */
+@Component
+public class SaOAuth2InterfaceImpl implements SaOAuth2Interface {
+
+
+ /*
+ * ------ 注意: 以下代码均为示例,真实环境需要根据数据库查询相关信息
+ */
+
+ // 返回此平台所有权限集合
+ @Override
+ public List getAppScopeList() {
+ return Arrays.asList("userinfo");
+ }
+
+ // 返回指定Client签约的所有Scope集合
+ @Override
+ public List getClientScopeList(String clientId) {
+ return Arrays.asList("userinfo");
+ }
+
+ // 获取指定 LoginId 对指定 Client 已经授权过的所有 Scope
+ @Override
+ public List getGrantScopeList(Object loginId, String clientId) {
+ return Arrays.asList();
+ }
+
+ // 返回指定Client允许的回调域名, 多个用逗号隔开, *代表不限制
+ @Override
+ public String getClientDomain(String clientId) {
+ return "*";
+ }
+
+ // 返回指定ClientId的ClientSecret
+ @Override
+ public String getClientSecret(String clientId) {
+ return "aaaa-bbbb-cccc-dddd-eeee";
+ }
+
+ // 根据ClientId和LoginId返回openid
+ @Override
+ public String getOpenid(String clientId, Object loginId) {
+ return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
+ }
+
+ // 根据ClientId和openid返回LoginId
+ @Override
+ public Object getLoginId(String clientId, String openid) {
+ return 10001;
+ }
+
+
+
+ /*
+ * 以上函数为开发时必须重写实现,其余函数可以按需重写
+ */
+
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2SpringAutowired.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2SpringAutowired.java
new file mode 100644
index 00000000..2da17816
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2SpringAutowired.java
@@ -0,0 +1,53 @@
+package com.pj.oauth2;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+import cn.dev33.satoken.oauth2.SaOAuth2Manager;
+import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
+import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
+
+/**
+ * 利用Spring完成自动装配
+ *
+ * @author kong
+ *
+ */
+@Component
+public class SaOAuth2SpringAutowired {
+
+ /**
+ * 获取OAuth2配置Bean
+ *
+ * @return 配置对象
+ */
+ @Bean
+ @ConfigurationProperties(prefix = "spring.sa-token.oauth2")
+ public SaOAuth2Config getSaOAuth2Config() {
+ return new SaOAuth2Config();
+ }
+
+ /**
+ * 注入OAuth2配置Bean
+ *
+ * @param saOAuth2Config 配置对象
+ */
+ @Autowired
+ public void setSaOAuth2Config(SaOAuth2Config saOAuth2Config) {
+ SaOAuth2Manager.setConfig(saOAuth2Config);
+ }
+
+ /**
+ * 注入OAuth2接口Bean
+ *
+ * @param saOAuth2Interface OAuth2接口Bean
+ */
+ @Autowired(required = false)
+ public void setSaOAuth2Interface(SaOAuth2Interface saOAuth2Interface) {
+ SaOAuth2Manager.setInterface(saOAuth2Interface);
+ }
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/AjaxJson.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/AjaxJson.java
new file mode 100644
index 00000000..5d39ac65
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/AjaxJson.java
@@ -0,0 +1,223 @@
+package com.pj.utils;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+
+/**
+ * ajax请求返回Json格式数据的封装
+ * 所有预留字段:
+ * code=状态码
+ * msg=描述信息
+ * data=携带对象
+ * pageNo=当前页
+ * pageSize=页大小
+ * startIndex=起始索引
+ * dataCount=数据总数
+ * pageCount=分页总数
+ * 返回范例:
+ *
+ {
+ "code": 200, // 成功时=200, 失败时=500 msg=失败原因
+ "msg": "ok",
+ "data": {}
+ }
+
+ */
+public class AjaxJson extends LinkedHashMap implements Serializable{
+
+ private static final long serialVersionUID = 1L; // 序列化版本号
+
+ public static final int CODE_SUCCESS = 200; // 成功状态码
+ public static final int CODE_ERROR = 500; // 错误状态码
+ public static final int CODE_WARNING = 501; // 警告状态码
+ public static final int CODE_NOT_JUR = 403; // 无权限状态码
+ public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
+ public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
+
+
+
+ // ============================ 写值取值 ==================================
+
+ /** 给code赋值,连缀风格 */
+ public AjaxJson setCode(int code) {
+ this.put("code", code);
+ return this;
+ }
+ /** 返回code */
+ public Integer getCode() {
+ return (Integer)this.get("code");
+ }
+
+ /** 给msg赋值,连缀风格 */
+ public AjaxJson setMsg(String msg) {
+ this.put("msg", msg);
+ return this;
+ }
+ /** 获取msg */
+ public String getMsg() {
+ return (String)this.get("msg");
+ }
+
+ /** 给data赋值,连缀风格 */
+ public AjaxJson setData(Object data) {
+ this.put("data", data);
+ return this;
+ }
+ /** 获取data */
+ public String getData() {
+ return (String)this.get("data");
+ }
+ /** 将data还原为指定类型并返回 */
+ @SuppressWarnings("unchecked")
+ public T getData(Class cs) {
+ return (T) this.getData();
+ }
+
+ /** 给dataCount(数据总数)赋值,连缀风格 */
+ public AjaxJson setDataCount(Long dataCount) {
+ this.put("dataCount", dataCount);
+ // 如果提供了数据总数,则尝试计算page信息
+ if(dataCount != null && dataCount >= 0) {
+ // 如果:已有page信息
+ if(get("pageNo") != null) {
+ this.initPageInfo();
+ }
+// // 或者:是JavaWeb环境
+// else if(SoMap.isJavaWeb()) {
+// SoMap so = SoMap.getRequestSoMap();
+// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
+// this.initPageInfo();
+// }
+ }
+ return this;
+ }
+ /** 获取dataCount(数据总数) */
+ public String getDataCount() {
+ return (String)this.get("dataCount");
+ }
+
+ /** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
+ public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
+ this.put("pageNo", pageNo);
+ this.put("pageSize", pageSize);
+ return this;
+ }
+
+ /** 根据 pageSize dataCount,计算startIndex 与 pageCount */
+ public AjaxJson initPageInfo() {
+ long pageNo = (long)this.get("pageNo");
+ long pageSize = (long)this.get("pageSize");
+ long dataCount = (long)this.get("dataCount");
+ this.set("startIndex", (pageNo - 1) * pageSize);
+ long pc = dataCount / pageSize;
+ this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
+ return this;
+ }
+
+
+ /** 写入一个值 自定义key, 连缀风格 */
+ public AjaxJson set(String key, Object data) {
+ this.put(key, data);
+ return this;
+ }
+
+ /** 写入一个Map, 连缀风格 */
+ public AjaxJson setMap(Map map) {
+ for (String key : map.keySet()) {
+ this.put(key, map.get(key));
+ }
+ return this;
+ }
+
+
+ // ============================ 构建 ==================================
+
+ public AjaxJson(int code, String msg, Object data, Long dataCount) {
+ this.setCode(code);
+ this.setMsg(msg);
+ this.setData(data);
+ this.setDataCount(dataCount == null ? -1 : dataCount);
+ }
+
+ /** 返回成功 */
+ public static AjaxJson getSuccess() {
+ return new AjaxJson(CODE_SUCCESS, "ok", null, null);
+ }
+ public static AjaxJson getSuccess(String msg) {
+ return new AjaxJson(CODE_SUCCESS, msg, null, null);
+ }
+ public static AjaxJson getSuccess(String msg, Object data) {
+ return new AjaxJson(CODE_SUCCESS, msg, data, null);
+ }
+ public static AjaxJson getSuccessData(Object data) {
+ return new AjaxJson(CODE_SUCCESS, "ok", data, null);
+ }
+
+
+ /** 返回失败 */
+ public static AjaxJson getError() {
+ return new AjaxJson(CODE_ERROR, "error", null, null);
+ }
+ public static AjaxJson getError(String msg) {
+ return new AjaxJson(CODE_ERROR, msg, null, null);
+ }
+
+ /** 返回警告 */
+ public static AjaxJson getWarning() {
+ return new AjaxJson(CODE_ERROR, "warning", null, null);
+ }
+ public static AjaxJson getWarning(String msg) {
+ return new AjaxJson(CODE_WARNING, msg, null, null);
+ }
+
+ /** 返回未登录 */
+ public static AjaxJson getNotLogin() {
+ return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
+ }
+
+ /** 返回没有权限的 */
+ public static AjaxJson getNotJur(String msg) {
+ return new AjaxJson(CODE_NOT_JUR, msg, null, null);
+ }
+
+ /** 返回一个自定义状态码的 */
+ public static AjaxJson get(int code, String msg){
+ return new AjaxJson(code, msg, null, null);
+ }
+
+ /** 返回分页和数据的 */
+ public static AjaxJson getPageData(Long dataCount, Object data){
+ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
+ }
+
+ /** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
+ public static AjaxJson getByLine(int line){
+ if(line > 0){
+ return getSuccess("ok", line);
+ }
+ return getError("error").setData(line);
+ }
+
+ /** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */
+ public static AjaxJson getByBoolean(boolean b){
+ return b ? getSuccess("ok") : getError("error");
+ }
+
+
+
+
+
+
+
+// // 历史版本遗留代码
+// public int code; // 状态码
+// public String msg; // 描述信息
+// public Object data; // 携带对象
+// public Long dataCount; // 数据总数,用于分页
+
+
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/SoMap.java b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/SoMap.java
new file mode 100644
index 00000000..e4524216
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/utils/SoMap.java
@@ -0,0 +1,723 @@
+package com.pj.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦
+ * 所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用
+ *
最新:2020-12-10 新增部分构造方法
+ * @author kong
+ */
+public class SoMap extends LinkedHashMap {
+
+ private static final long serialVersionUID = 1L;
+
+ public SoMap() {
+ }
+
+
+ /** 以下元素会在isNull函数中被判定为Null, */
+ public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
+ public static final List NULL_ELEMENT_LIST;
+
+
+ static {
+ NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
+ }
+
+ // ============================= 读值 =============================
+
+ /** 获取一个值 */
+ @Override
+ public Object get(Object key) {
+ if("this".equals(key)) {
+ return this;
+ }
+ return super.get(key);
+ }
+
+ /** 如果为空,则返回默认值 */
+ public Object get(Object key, Object defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ /** 转为String并返回 */
+ public String getString(String key) {
+ Object value = get(key);
+ if(value == null) {
+ return null;
+ }
+ return String.valueOf(value);
+ }
+
+ /** 如果为空,则返回默认值 */
+ public String getString(String key, String defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return String.valueOf(value);
+ }
+
+ /** 转为int并返回 */
+ public int getInt(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0;
+ }
+ return Integer.valueOf(String.valueOf(value));
+ }
+ /** 转为int并返回,同时指定默认值 */
+ public int getInt(String key, int defaultValue) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return defaultValue;
+ }
+ return Integer.valueOf(String.valueOf(value));
+ }
+
+ /** 转为long并返回 */
+ public long getLong(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0;
+ }
+ return Long.valueOf(String.valueOf(value));
+ }
+
+ /** 转为double并返回 */
+ public double getDouble(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return 0.0;
+ }
+ return Double.valueOf(String.valueOf(value));
+ }
+
+ /** 转为boolean并返回 */
+ public boolean getBoolean(String key) {
+ Object value = get(key);
+ if(valueIsNull(value)) {
+ return false;
+ }
+ return Boolean.valueOf(String.valueOf(value));
+ }
+
+ /** 转为Date并返回,根据自定义格式 */
+ public Date getDateByFormat(String key, String format) {
+ try {
+ return new SimpleDateFormat(format).parse(getString(key));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** 转为Date并返回,根据格式: yyyy-MM-dd */
+ public Date getDate(String key) {
+ return getDateByFormat(key, "yyyy-MM-dd");
+ }
+
+ /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */
+ public Date getDateTime(String key) {
+ return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
+ }
+
+ /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
+ @SuppressWarnings("unchecked")
+ public List getList(String key) {
+ Object value = get(key);
+ List list = null;
+ if(value == null || value.equals("")) {
+ list = new ArrayList();
+ }
+ else if(value instanceof List) {
+ list = (List)value;
+ } else {
+ list = new ArrayList();
+ list.add(value);
+ }
+ return list;
+ }
+
+ /** 获取集合 (指定泛型类型) */
+ public List getList(String key, Class cs) {
+ List list = getList(key);
+ List list2 = new ArrayList();
+ for (Object obj : list) {
+ T objC = getValueByClass(obj, cs);
+ list2.add(objC);
+ }
+ return list2;
+ }
+
+ /** 获取集合(逗号分隔式),(指定类型) */
+ public List getListByComma(String key, Class cs) {
+ String listStr = getString(key);
+ if(listStr == null || listStr.equals("")) {
+ return new ArrayList<>();
+ }
+ // 开始转化
+ String [] arr = listStr.split(",");
+ List list = new ArrayList();
+ for (String str : arr) {
+ if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {
+ str = str.trim();
+ }
+ T objC = getValueByClass(str, cs);
+ list.add(objC);
+ }
+ return list;
+ }
+
+
+ /** 根据指定类型从map中取值,返回实体对象 */
+ public T getModel(Class cs) {
+ try {
+ return getModelByObject(cs.newInstance());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** 从map中取值,塞到一个对象中 */
+ public T getModelByObject(T obj) {
+ // 获取类型
+ Class> cs = obj.getClass();
+ // 循环复制
+ for (Field field : cs.getDeclaredFields()) {
+ try {
+ // 获取对象
+ Object value = this.get(field.getName());
+ if(value == null) {
+ continue;
+ }
+ field.setAccessible(true);
+ Object valueConvert = getValueByClass(value, field.getType());
+ field.set(obj, valueConvert);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException("属性取值出错:" + field.getName(), e);
+ }
+ }
+ return obj;
+ }
+
+
+
+ /**
+ * 将指定值转化为指定类型并返回
+ * @param obj
+ * @param cs
+ * @param
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static T getValueByClass(Object obj, Class cs) {
+ String obj2 = String.valueOf(obj);
+ Object obj3 = null;
+ if (cs.equals(String.class)) {
+ obj3 = obj2;
+ } else if (cs.equals(int.class) || cs.equals(Integer.class)) {
+ obj3 = Integer.valueOf(obj2);
+ } else if (cs.equals(long.class) || cs.equals(Long.class)) {
+ obj3 = Long.valueOf(obj2);
+ } else if (cs.equals(short.class) || cs.equals(Short.class)) {
+ obj3 = Short.valueOf(obj2);
+ } else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
+ obj3 = Byte.valueOf(obj2);
+ } else if (cs.equals(float.class) || cs.equals(Float.class)) {
+ obj3 = Float.valueOf(obj2);
+ } else if (cs.equals(double.class) || cs.equals(Double.class)) {
+ obj3 = Double.valueOf(obj2);
+ } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
+ obj3 = Boolean.valueOf(obj2);
+ } else {
+ obj3 = (T)obj;
+ }
+ return (T)obj3;
+ }
+
+
+ // ============================= 写值 =============================
+
+ /**
+ * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去)
+ */
+ public void setDefaultValue(String key, Object defaultValue) {
+ if(isNull(key)) {
+ set(key, defaultValue);
+ }
+ }
+
+ /** set一个值,连缀风格 */
+ public SoMap set(String key, Object value) {
+ // 防止敏感key
+ if(key.toLowerCase().equals("this")) {
+ return this;
+ }
+ put(key, value);
+ return this;
+ }
+
+ /** 将一个Map塞进SoMap */
+ public SoMap setMap(Map map) {
+ if(map != null) {
+ for (String key : map.keySet()) {
+ this.set(key, map.get(key));
+ }
+ }
+ return this;
+ }
+
+ /** 将一个对象解析塞进SoMap */
+ public SoMap setModel(Object model) {
+ if(model == null) {
+ return this;
+ }
+ Field[] fields = model.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ try{
+ field.setAccessible(true);
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ if(!isStatic) {
+ this.set(field.getName(), field.get(model));
+ }
+ }catch (Exception e){
+ throw new RuntimeException(e);
+ }
+ }
+ return this;
+ }
+
+ /** 将json字符串解析后塞进SoMap */
+ public SoMap setJsonString(String jsonString) {
+ try {
+ @SuppressWarnings("unchecked")
+ Map map = new ObjectMapper().readValue(jsonString, Map.class);
+ return this.setMap(map);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ // ============================= 删值 =============================
+
+ /** delete一个值,连缀风格 */
+ public SoMap delete(String key) {
+ remove(key);
+ return this;
+ }
+
+ /** 清理所有value为null的字段 */
+ public SoMap clearNull() {
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(this.isNull(key)) {
+ iterator.remove();
+ this.remove(key);
+ }
+
+ }
+ return this;
+ }
+ /** 清理指定key */
+ public SoMap clearIn(String ...keys) {
+ List keys2 = Arrays.asList(keys);
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(keys2.contains(key) == true) {
+ iterator.remove();
+ this.remove(key);
+ }
+ }
+ return this;
+ }
+ /** 清理掉不在列表中的key */
+ public SoMap clearNotIn(String ...keys) {
+ List keys2 = Arrays.asList(keys);
+ Iterator iterator = this.keySet().iterator();
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if(keys2.contains(key) == false) {
+ iterator.remove();
+ this.remove(key);
+ }
+
+ }
+ return this;
+ }
+ /** 清理掉所有key */
+ public SoMap clearAll() {
+ clear();
+ return this;
+ }
+
+
+ // ============================= 快速构建 =============================
+
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap() {
+ return new SoMap();
+ }
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap(String key, Object value) {
+ return new SoMap().set(key, value);
+ }
+ /** 构建一个SoMap并返回 */
+ public static SoMap getSoMap(Map map) {
+ return new SoMap().setMap(map);
+ }
+
+ /** 将一个对象集合解析成为SoMap */
+ public static SoMap getSoMapByModel(Object model) {
+ return SoMap.getSoMap().setModel(model);
+ }
+
+ /** 将一个对象集合解析成为SoMap集合 */
+ public static List getSoMapByList(List> list) {
+ List listMap = new ArrayList();
+ for (Object model : list) {
+ listMap.add(getSoMapByModel(model));
+ }
+ return listMap;
+ }
+
+ /** 克隆指定key,返回一个新的SoMap */
+ public SoMap cloneKeys(String... keys) {
+ SoMap so = new SoMap();
+ for (String key : keys) {
+ so.set(key, this.get(key));
+ }
+ return so;
+ }
+ /** 克隆所有key,返回一个新的SoMap */
+ public SoMap cloneSoMap() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key, this.get(key));
+ }
+ return so;
+ }
+
+ /** 将所有key转为大写 */
+ public SoMap toUpperCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key.toUpperCase(), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key转为小写 */
+ public SoMap toLowerCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(key.toLowerCase(), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中下划线转为中划线模式 (kebab-case风格) */
+ public SoMap toKebabCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordEachKebabCase(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中下划线转为小驼峰模式 */
+ public SoMap toHumpCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordEachBigFs(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+ /** 将所有key中小驼峰转为下划线模式 */
+ public SoMap humpToLineCase() {
+ SoMap so = new SoMap();
+ for (String key : this.keySet()) {
+ so.set(wordHumpToLine(key), this.get(key));
+ }
+ this.clearAll().setMap(so);
+ return this;
+ }
+
+
+
+
+ // ============================= 辅助方法 =============================
+
+
+ /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */
+ public boolean isNull(String key) {
+ return valueIsNull(get(key));
+ }
+
+ /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */
+ public boolean isContainNull(String ...keys) {
+ for (String key : keys) {
+ if(this.isNull(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** 与isNull()相反 */
+ public boolean isNotNull(String key) {
+ return !isNull(key);
+ }
+ /** 指定key的value是否为null,作用同isNotNull() */
+ public boolean has(String key) {
+ return !isNull(key);
+ }
+
+ /** 指定value在此SoMap的判断标准中是否为null */
+ public boolean valueIsNull(Object value) {
+ return NULL_ELEMENT_LIST.contains(value);
+ }
+
+ /** 验证指定key不为空,为空则抛出异常 */
+ public SoMap checkNull(String ...keys) {
+ for (String key : keys) {
+ if(this.isNull(key)) {
+ throw new RuntimeException("参数" + key + "不能为空");
+ }
+ }
+ return this;
+ }
+
+ static Pattern patternNumber = Pattern.compile("[0-9]*");
+ /** 指定key是否为数字 */
+ public boolean isNumber(String key) {
+ String value = getString(key);
+ if(value == null) {
+ return false;
+ }
+ return patternNumber.matcher(value).matches();
+ }
+
+
+
+
+ /**
+ * 转为JSON字符串
+ */
+ public String toJsonString() {
+ try {
+// SoMap so = SoMap.getSoMap(this);
+ return new ObjectMapper().writeValueAsString(this);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+// /**
+// * 转为JSON字符串, 带格式的
+// */
+// public String toJsonFormatString() {
+// try {
+// return JSON.toJSONString(this, true);
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+// }
+
+ // ============================= web辅助 =============================
+
+
+ /**
+ * 返回当前request请求的的所有参数
+ * @return
+ */
+ public static SoMap getRequestSoMap() {
+ // 大善人SpringMVC提供的封装
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if(servletRequestAttributes == null) {
+ throw new RuntimeException("当前线程非JavaWeb环境");
+ }
+ // 当前request
+ HttpServletRequest request = servletRequestAttributes.getRequest();
+ if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) {
+ initRequestSoMap(request);
+ }
+ return (SoMap)request.getAttribute("currentSoMap");
+ }
+
+ /** 初始化当前request的 SoMap */
+ private static void initRequestSoMap(HttpServletRequest request) {
+ SoMap soMap = new SoMap();
+ Map parameterMap = request.getParameterMap(); // 获取所有参数
+ for (String key : parameterMap.keySet()) {
+ try {
+ String[] values = parameterMap.get(key); // 获得values
+ if(values.length == 1) {
+ soMap.set(key, values[0]);
+ } else {
+ List list = new ArrayList();
+ for (String v : values) {
+ list.add(v);
+ }
+ soMap.set(key, list);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ request.setAttribute("currentSoMap", soMap);
+ }
+
+ /**
+ * 验证返回当前线程是否为JavaWeb环境
+ * @return
+ */
+ public static boolean isJavaWeb() {
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装
+ if(servletRequestAttributes == null) {
+ return false;
+ }
+ return true;
+ }
+
+
+
+ // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) =============================
+
+ /** get 当前页 */
+ public int getKeyPageNo() {
+ int pageNo = getInt("pageNo", 1);
+ if(pageNo <= 0) {
+ pageNo = 1;
+ }
+ return pageNo;
+ }
+ /** get 页大小 */
+ public int getKeyPageSize() {
+ int pageSize = getInt("pageSize", 10);
+ if(pageSize <= 0 || pageSize > 1000) {
+ pageSize = 10;
+ }
+ return pageSize;
+ }
+
+ /** get 排序方式 */
+ public int getKeySortType() {
+ return getInt("sortType");
+ }
+
+
+
+
+
+
+ // ============================= 工具方法 =============================
+
+
+ /**
+ * 将一个一维集合转换为树形集合
+ * @param list 集合
+ * @param idKey id标识key
+ * @param parentIdKey 父id标识key
+ * @param childListKey 子节点标识key
+ * @return 转换后的tree集合
+ */
+ public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) {
+ // 声明新的集合,存储tree形数据
+ List newTreeList = new ArrayList();
+ // 声明hash-Map,方便查找数据
+ SoMap hash = new SoMap();
+ // 将数组转为Object的形式,key为数组中的id
+ for (int i = 0; i < list.size(); i++) {
+ SoMap json = (SoMap) list.get(i);
+ hash.put(json.getString(idKey), json);
+ }
+ // 遍历结果集
+ for (int j = 0; j < list.size(); j++) {
+ // 单条记录
+ SoMap aVal = (SoMap) list.get(j);
+ // 在hash中取出key为单条记录中pid的值
+ SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString());
+ // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
+ if (hashVp != null) {
+ // 检查是否有child属性,有则添加,没有则新建
+ if (hashVp.get(childListKey) != null) {
+ @SuppressWarnings("unchecked")
+ List ch = (List) hashVp.get(childListKey);
+ ch.add(aVal);
+ hashVp.put(childListKey, ch);
+ } else {
+ List ch = new ArrayList();
+ ch.add(aVal);
+ hashVp.put(childListKey, ch);
+ }
+ } else {
+ newTreeList.add(aVal);
+ }
+ }
+ return newTreeList;
+ }
+
+
+
+ /** 指定字符串的字符串下划线转大写模式 */
+ private static String wordEachBig(String str){
+ String newStr = "";
+ for (String s : str.split("_")) {
+ newStr += wordFirstBig(s);
+ }
+ return newStr;
+ }
+ /** 返回下划线转小驼峰形式 */
+ private static String wordEachBigFs(String str){
+ return wordFirstSmall(wordEachBig(str));
+ }
+
+ /** 将指定单词首字母大写 */
+ private static String wordFirstBig(String str) {
+ return str.substring(0, 1).toUpperCase() + str.substring(1, str.length());
+ }
+
+ /** 将指定单词首字母小写 */
+ private static String wordFirstSmall(String str) {
+ return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
+ }
+
+ /** 下划线转中划线 */
+ private static String wordEachKebabCase(String str) {
+ return str.replaceAll("_", "-");
+ }
+
+ /** 驼峰转下划线 */
+ private static String wordHumpToLine(String str) {
+ return str.replaceAll("[A-Z]", "_$0").toLowerCase();
+ }
+
+
+}
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml
new file mode 100644
index 00000000..4424f2d3
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml
@@ -0,0 +1,41 @@
+server:
+ port: 8001
+
+spring:
+ # 静态文件路径映射
+ resources:
+ static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
+ # static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-server\src\main\resources\static\
+
+ # sa-token配置
+ sa-token:
+ # token名称 (同时也是cookie名称)
+ token-name: satoken-server
+
+
+ # redis配置
+ redis:
+ # Redis数据库索引(默认为0)
+ database: 1
+ # Redis服务器地址
+ host: 127.0.0.1
+ # Redis服务器连接端口
+ port: 6379
+ # Redis服务器连接密码(默认为空)
+ # password:
+ # 连接超时时间(毫秒)
+ timeout: 1000ms
+ lettuce:
+ pool:
+ # 连接池最大连接数
+ max-active: 200
+ # 连接池最大阻塞等待时间(使用负值表示没有限制)
+ max-wait: -1ms
+ # 连接池中的最大空闲连接
+ max-idle: 10
+ # 连接池中的最小空闲连接
+ min-idle: 0
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/auth.html b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/auth.html
new file mode 100644
index 00000000..c4c733c7
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/auth.html
@@ -0,0 +1,93 @@
+
+
+
+
+ 服务提供方-确认授权页
+
+
+
+
+
服务提供方-确认授权页
+
+
应用id:
+
请求授权:
+
+
是否同意授权:
+
+ 同意
+ 拒绝
+
+
+
+
+
+
+
diff --git a/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/login.html b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/login.html
new file mode 100644
index 00000000..d7a43c22
--- /dev/null
+++ b/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/static/login.html
@@ -0,0 +1,53 @@
+
+
+
+
+ 服务提供方-登录页
+
+
+
+
+
服务提供方-登录页
+
注:您当前在服务提供方尚未登录,请先登录
+
测试账号: test test
+ 账号:
+ 密码:
+
登录
+
+
+
+
+
diff --git a/sa-token-oauth2/.gitignore b/sa-token-oauth2/.gitignore
new file mode 100644
index 00000000..8122f47c
--- /dev/null
+++ b/sa-token-oauth2/.gitignore
@@ -0,0 +1,13 @@
+target/
+
+node_modules/
+bin/
+.settings/
+unpackage/
+.classpath
+.project
+
+.factorypath
+
+.idea/
+.iml
\ No newline at end of file
diff --git a/sa-token-oauth2/README.md b/sa-token-oauth2/README.md
new file mode 100644
index 00000000..5659b6ed
--- /dev/null
+++ b/sa-token-oauth2/README.md
@@ -0,0 +1,11 @@
+# sa-token-oauth2 内测版
+
+sa-token-oauth2 模块是 sa-token 实现 oauth2.0 的部分,目前该模块功能完成度较低,为避免不可预知的风险,建议仅做学习测试使用
+
+## 启动步骤
+
+1. 启动项目 `sa-token-demo-oauth2-server`, 此为OAuth2.0的服务提供方
+2. 启动项目 `sa-token-demo-oauth2-client`, 此为OAuth2.0的客户端
+3. 根据控制台打印,访问测试地址即可:[http://localhost:8002/login.html](http://localhost:8002/login.html)
+
+可结合代码注释学习查看
diff --git a/sa-token-oauth2/pom.xml b/sa-token-oauth2/pom.xml
new file mode 100644
index 00000000..666c7fe7
--- /dev/null
+++ b/sa-token-oauth2/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+
+ cn.dev33
+ sa-token-parent
+ 1.13.0
+
+ jar
+
+ sa-token-dao-redis
+ sa-token-oauth2
+ 1.13.0-alpha
+ sa-token realization oauth2.0
+
+
+
+
+ cn.dev33
+ sa-token-core
+ ${sa-token-version}
+
+
+
+
+
+
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java
new file mode 100644
index 00000000..4cf80017
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java
@@ -0,0 +1,54 @@
+package cn.dev33.satoken.oauth2;
+
+import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
+import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
+import cn.dev33.satoken.oauth2.logic.SaOAuth2InterfaceDefaultImpl;
+
+/**
+ * sa-token oauth2 模块 总控类
+ *
+ * @author kong
+ *
+ */
+public class SaOAuth2Manager {
+
+ /**
+ * OAuth2 配置 Bean
+ */
+ private static SaOAuth2Config config;
+ public static SaOAuth2Config getConfig() {
+ if (config == null) {
+ // 初始化默认值
+ synchronized (SaOAuth2Manager.class) {
+ if (config == null) {
+ setConfig(new SaOAuth2Config());
+ }
+ }
+ }
+ return config;
+ }
+ public static void setConfig(SaOAuth2Config config) {
+ SaOAuth2Manager.config = config;
+ }
+
+ /**
+ * sa-token-oauth2 逻辑 Bean
+ */
+ private static SaOAuth2Interface saOAuth2Interface;
+ public static SaOAuth2Interface getInterface() {
+ if (saOAuth2Interface == null) {
+ // 初始化默认值
+ synchronized (SaOAuth2Manager.class) {
+ if (saOAuth2Interface == null) {
+ setInterface(new SaOAuth2InterfaceDefaultImpl());
+ }
+ }
+ }
+ return saOAuth2Interface;
+ }
+ public static void setInterface(SaOAuth2Interface interfaceObj) {
+ SaOAuth2Manager.saOAuth2Interface = interfaceObj;
+ }
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java
new file mode 100644
index 00000000..b911babb
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java
@@ -0,0 +1,73 @@
+package cn.dev33.satoken.oauth2.config;
+
+/**
+ * sa-token oauth2 配置类 Model
+ * @author kong
+ *
+ */
+public class SaOAuth2Config {
+
+ /**
+ * 授权码默认保存的时间(单位秒) 默认五分钟
+ */
+ private long codeTimeout = 60 * 5;
+
+ /**
+ * access_token默认保存的时间(单位秒) 默认两个小时
+ */
+ private long accessTokenTimeout = 60 * 60 * 2;
+
+ /**
+ * refresh_token默认保存的时间(单位秒) 默认30 天
+ */
+ private long refreshTokenTimeout = 60 * 60 * 24 * 30;
+
+
+ /**
+ * @return codeTimeout
+ */
+ public long getCodeTimeout() {
+ return codeTimeout;
+ }
+
+ /**
+ * @param codeTimeout 要设置的 codeTimeout
+ */
+ public void setCodeTimeout(long codeTimeout) {
+ this.codeTimeout = codeTimeout;
+ }
+
+ /**
+ * @return accessTokenTimeout
+ */
+ public long getAccessTokenTimeout() {
+ return accessTokenTimeout;
+ }
+
+ /**
+ * @param accessTokenTimeout 要设置的 accessTokenTimeout
+ */
+ public void setAccessTokenTimeout(long accessTokenTimeout) {
+ this.accessTokenTimeout = accessTokenTimeout;
+ }
+
+ /**
+ * @return refreshTokenTimeout
+ */
+ public long getRefreshTokenTimeout() {
+ return refreshTokenTimeout;
+ }
+
+ /**
+ * @param refreshTokenTimeout 要设置的 refreshTokenTimeout
+ */
+ public void setRefreshTokenTimeout(long refreshTokenTimeout) {
+ this.refreshTokenTimeout = refreshTokenTimeout;
+ }
+
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Interface.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Interface.java
new file mode 100644
index 00000000..11697dbb
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Interface.java
@@ -0,0 +1,553 @@
+package cn.dev33.satoken.oauth2.logic;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+import cn.dev33.satoken.SaTokenManager;
+import cn.dev33.satoken.exception.SaTokenException;
+import cn.dev33.satoken.oauth2.SaOAuth2Manager;
+import cn.dev33.satoken.oauth2.model.AccessTokenModel;
+import cn.dev33.satoken.oauth2.model.CodeModel;
+import cn.dev33.satoken.oauth2.model.RequestAuthModel;
+import cn.dev33.satoken.oauth2.util.SaOAuth2Consts;
+import cn.dev33.satoken.oauth2.util.SaOAuth2InsideUtil;
+import cn.dev33.satoken.util.SaTokenInsideUtil;
+
+/**
+ * sa-token-oauth2 模块 逻辑接口
+ * @author kong
+ *
+ */
+public interface SaOAuth2Interface {
+
+
+ // ------------------- 获取数据
+
+ /**
+ * [default] 返回此平台所有权限集合
+ * @return 此平台所有权限名称集合
+ */
+ public default List getAppScopeList() {
+ return Arrays.asList("userinfo");
+ }
+
+ /**
+ * [default] 返回指定Client签约的所有Scope名称集合
+ * @param clientId 应用id
+ * @return Scope集合
+ */
+ public default List getClientScopeList(String clientId) {
+ // 默认返回此APP的所有权限
+ return getAppScopeList();
+ }
+
+ /**
+ * [default] 获取指定 LoginId 对指定 Client 已经授权过的所有 Scope
+ * @param clientId 应用id
+ * @param loginId 账号id
+ * @return Scope集合
+ */
+ public default List getGrantScopeList(Object loginId, String clientId) {
+ // 默认返回空集合
+ return Arrays.asList();
+ }
+
+ /**
+ * [default] 返回指定Client允许的回调域名, 多个用逗号隔开, *代表不限制
+ * @param clientId 应用id
+ * @return domain集合
+ */
+ public default String getClientDomain(String clientId) {
+ return "*";
+ }
+
+ /**
+ * [default] 返回指定ClientId的ClientSecret
+ * @param clientId 应用id
+ * @return 此应用的秘钥
+ */
+ public default String getClientSecret(String clientId) {
+ return null;
+ }
+
+ /**
+ * [default] 根据ClientId和LoginId返回openid
+ * @param clientId 应用id
+ * @param loginId 账号id
+ * @return 此账号在此Client下的openid
+ */
+ public default String getOpenid(String clientId, Object loginId) {
+ return null;
+ }
+
+ /**
+ * [default] 根据ClientId和openid返回LoginId
+ * @param clientId 应用id
+ * @param openid openid
+ * @return LoginId
+ */
+ public default Object getLoginId(String clientId, String openid) {
+ return null;
+ }
+
+
+ // ------------------- 数据校验
+
+ /**
+ * [default] 检查一个 Client 是否签约了指定的Scope
+ * @param clientId 应用id
+ * @param scope 权限
+ */
+ public default void checkContract(String clientId, String scope) {
+ List clientScopeList = getClientScopeList(clientId);
+ List scopelist = Arrays.asList(scope.split(","));
+ if(clientScopeList.containsAll(scopelist) == false) {
+ throw new SaTokenException("请求授权范围超出或无效");
+ }
+ }
+
+ /**
+ * [default] 指定 loginId 是否对一个 Client 授权给了指定 Scope
+ * @param loginId 账号id
+ * @param clientId 应用id
+ * @param scope 权限
+ * @return 是否已经授权
+ */
+ public default boolean isGrant(Object loginId, String clientId, String scope) {
+ List grantScopeList = getGrantScopeList(loginId, clientId);
+ List scopeList = convertStringToList(scope);
+ return grantScopeList.containsAll(scopeList);
+ }
+
+ /**
+ * [default] 指定Client使用指定url作为回调地址,是否合法
+ * @param clientId 应用id
+ * @param url 指定url
+ */
+ public default void checkRightUrl(String clientId, String url) {
+ // 首先检测url格式
+ if(SaOAuth2InsideUtil.isUrl(url) == false) {
+ throw new SaTokenException("url格式错误");
+ }
+ // ---- 检测
+
+ // 获取此应用允许的域名列表
+ String domain = getClientDomain(clientId);
+ // 如果是null或者空字符串, 则代表任何域名都无法通过检查
+ if(domain == null || "".equals(domain)) {
+ throw new SaTokenException("重定向地址无效");
+ }
+ // 如果是*符号,代表允许任何域名
+ if(SaOAuth2Consts.UNLIMITED_DOMAIN.equals(domain)) {
+ return;
+ }
+
+ // 获取域名进行比对
+ try {
+ String host = new URL(url).getHost();
+ List domainList = convertStringToList(domain);
+ if(domainList.contains(host) == false) {
+ throw new SaTokenException("重定向地址不在列表中");
+ }
+ } catch (MalformedURLException e) {
+ throw new SaTokenException("url格式错误", e);
+ }
+ }
+
+ /**
+ * [default] 校验code、clientId、clientSecret 三者是否正确
+ * @param code 授权码
+ * @param clientId 应用id
+ * @param clientSecret 秘钥
+ * @return CodeModel对象
+ */
+ public default CodeModel checkCodeIdSecret(String code, String clientId, String clientSecret) {
+
+ // 获取授权码信息
+ CodeModel codeModel = getCode(code);
+
+ // 验证code、client_id、client_secret
+ if(codeModel == null) {
+ throw new SaTokenException("无效code");
+ }
+
+ if(codeModel.getClientId().equals(clientId) == false){
+ throw new SaTokenException("无效client_id");
+ }
+ String dbClientSecret = getClientSecret(clientId);
+ System.out.println(dbClientSecret);
+ System.out.println(clientSecret);
+ if(dbClientSecret == null || dbClientSecret.equals(clientSecret) == false){
+ throw new SaTokenException("无效client_secret");
+ }
+
+ // 返回CodeMdoel
+ return codeModel;
+ }
+
+ /**
+ * [default] 校验access_token、clientId、clientSecret 三者是否正确
+ * @param accessToken access_token
+ * @param clientId 应用id
+ * @param clientSecret 秘钥
+ * @return AccessTokenModel对象
+ */
+ public default AccessTokenModel checkTokenIdSecret(String accessToken, String clientId, String clientSecret) {
+
+ // 获取授权码信息
+ AccessTokenModel tokenModel = getAccessToken(accessToken);
+
+ // 验证code、client_id、client_secret
+ if(tokenModel == null) {
+ throw new SaTokenException("无效access_token");
+ }
+ if(tokenModel.getClientId().equals(clientId) == false){
+ throw new SaTokenException("无效client_id");
+ }
+ String dbClientSecret = getClientSecret(clientId);
+ if(dbClientSecret == null || dbClientSecret.equals(clientSecret)){
+ throw new SaTokenException("无效client_secret");
+ }
+
+ // 返回AccessTokenModel
+ return tokenModel;
+ }
+
+
+ // ------------------- 逻辑相关
+
+ // ---- 授权码
+ /**
+ * [default] 根据参数生成一个授权码并返回
+ * @param authModel 请求授权参数Model
+ * @return 授权码Model
+ */
+ public default CodeModel generateCode(RequestAuthModel authModel) {
+
+ // 获取参数
+ String clientId = authModel.getClientId();
+ String scope = authModel.getScope();
+ Object loginId = authModel.getLoginId();
+ String redirectUri = authModel.getRedirectUri();
+ String state = authModel.getState();
+
+ // ------ 参数校验
+ // 此Client是否签约了此Scope
+ checkContract(clientId, scope);
+
+ // 校验重定向域名的格式是否合法
+ checkRightUrl(clientId, redirectUri);
+
+ // ------ 开始生成code码
+ String code = createCode(clientId, scope, loginId);
+ CodeModel codeModel = new CodeModel(code, clientId, scope, loginId);
+
+ // 拼接授权后重定向的域名 (拼接code和state参数)
+ String url = splicingParame(redirectUri, "code=" + code);
+ if(state != null) {
+ url = splicingParame(url, "state=" + state);
+ }
+ codeModel.setRedirectUri(url);
+
+ // 拒绝授权时重定向的地址
+ codeModel.setRejectUri(splicingParame(redirectUri, "handle=reject"));
+
+ // 计算此Scope是否已经授权过了
+ codeModel.setIsConfirm(isGrant(loginId, clientId, scope));
+
+ // ------ 开始保存
+
+ // 将此授权码保存到DB
+ long codeTimeout = SaOAuth2Manager.getConfig().getCodeTimeout();
+ SaTokenManager.getSaTokenDao().setObject(getKeyCodeModel(code), codeModel, codeTimeout);
+
+ // 如果此[Client&账号]已经有code正在存储,则先删除它
+ String key = getKeyClientLoginId(loginId, clientId);
+ SaTokenManager.getSaTokenDao().delete(key);
+
+ // 将此[Client&账号]的最新授权码保存到DB中
+ // 以便于完成授权码覆盖操作: 保证每次只有最新的授权码有效
+ SaTokenManager.getSaTokenDao().set(key, code, codeTimeout);
+
+ // 返回
+ return codeModel;
+ }
+
+ /**
+ * [default] 根据授权码获得授权码Model
+ * @param code 授权码
+ * @return 授权码Model
+ */
+ public default CodeModel getCode(String code) {
+ return (CodeModel)SaTokenManager.getSaTokenDao().getObject(getKeyCodeModel(code));
+ }
+
+ /**
+ * [default] 手动更改授权码对象信息
+ * @param code 授权码
+ * @param codeModel 授权码Model
+ */
+ public default void updateCode(String code, CodeModel codeModel) {
+ SaTokenManager.getSaTokenDao().updateObject(getKeyCodeModel(code), codeModel);
+ }
+
+ /**
+ * [default] 确认授权一个code
+ * @param code 授权码
+ */
+ public default void confirmCode(String code) {
+ // 获取codeModel
+ CodeModel codeModel = getCode(code);
+ // 如果该code码已经确认
+ if(codeModel.getIsConfirm() == true) {
+ return;
+ }
+ // 进行确认
+ codeModel.setIsConfirm(true);
+ updateCode(code, codeModel);
+ }
+
+ /**
+ * [default] 删除一个授权码
+ * @param code 授权码
+ */
+ public default void deleteCode(String code) {
+ SaTokenManager.getSaTokenDao().deleteObject(getKeyCodeModel(code));
+ }
+
+
+ // ------------------- access_token 和 refresh_token 相关
+
+ /**
+ * [default] 根据授权码Model生成一个access_token
+ * @param codeModel 授权码Model
+ * @return AccessTokenModel
+ */
+ public default AccessTokenModel generateAccessToken(CodeModel codeModel) {
+
+ // 先校验
+ if(codeModel == null) {
+ throw new SaTokenException("无效code");
+ }
+ if(codeModel.getIsConfirm() == false) {
+ throw new SaTokenException("该code尚未授权");
+ }
+
+ // 获取 TokenModel 并保存
+ AccessTokenModel tokenModel = converCodeToAccessToken(codeModel);
+ SaTokenManager.getSaTokenDao().setObject(getKeyAccessToken(tokenModel.getAccessToken()), tokenModel, SaOAuth2Manager.getConfig().getAccessTokenTimeout());
+
+ // 将此 CodeModel 当做 refresh_token 保存下来
+ SaTokenManager.getSaTokenDao().setObject(getKeyRefreshToken(tokenModel.getRefreshToken()), codeModel, SaOAuth2Manager.getConfig().getRefreshTokenTimeout());
+
+ // 返回
+ return tokenModel;
+ }
+
+ /**
+ * [default] 根据 access_token 获得其Model详细信息
+ * @param accessToken access_token
+ * @return AccessTokenModel (授权码Model)
+ */
+ public default AccessTokenModel getAccessToken(String accessToken) {
+ return (AccessTokenModel)SaTokenManager.getSaTokenDao().getObject(getKeyAccessToken(accessToken));
+ }
+
+ /**
+ * [default] 根据 refresh_token 生成一个新的 access_token
+ * @param refreshToken refresh_token
+ * @return 新的 access_token
+ */
+ public default AccessTokenModel refreshAccessToken(String refreshToken) {
+ // 获取Model信息
+ CodeModel codeModel = getRefreshToken(refreshToken);
+ if(codeModel == null) {
+ throw new SaTokenException("无效refresh_token");
+ }
+ // 获取新的 AccessToken 并保存
+ AccessTokenModel tokenModel = converCodeToAccessToken(codeModel);
+ SaTokenManager.getSaTokenDao().setObject(getKeyAccessToken(tokenModel.getAccessToken()), tokenModel, SaOAuth2Manager.getConfig().getAccessTokenTimeout());
+
+ // 返回
+ return tokenModel;
+ }
+
+ /**
+ * [default] 根据 refresh_token 获得其Model详细信息 (授权码Model)
+ * @param refreshToken refresh_token
+ * @return RefreshToken (授权码Model)
+ */
+ public default CodeModel getRefreshToken(String refreshToken) {
+ return (CodeModel)SaTokenManager.getSaTokenDao().getObject(getKeyRefreshToken(refreshToken));
+ }
+
+ /**
+ * [default] 获取 access_token 的有效期
+ * @param accessToken access_token
+ * @return 有效期
+ */
+ public default long getAccessTokenExpiresIn(String accessToken) {
+ return SaTokenManager.getSaTokenDao().getObjectTimeout(getKeyAccessToken(accessToken));
+ }
+
+ /**
+ * [default] 获取 refresh_token 的有效期
+ * @param refreshToken refresh_token
+ * @return 有效期
+ */
+ public default long getRefreshTokenExpiresIn(String refreshToken) {
+ return SaTokenManager.getSaTokenDao().getObjectTimeout(getKeyRefreshToken(refreshToken));
+ }
+
+ /**
+ * [default] 获取 access_token 所代表的LoginId
+ * @param accessToken access_token
+ * @return LoginId
+ */
+ public default Object getLoginIdByAccessToken(String accessToken) {
+ AccessTokenModel tokenModel = SaOAuth2Util.getAccessToken(accessToken);
+ if(tokenModel == null) {
+ throw new SaTokenException("无效access_token");
+ }
+ return getLoginId(tokenModel.getClientId(), tokenModel.getOpenid());
+ }
+
+
+ // ------------------- 自定义策略相关
+
+ /**
+ * [default] 将指定字符串按照逗号分隔符转化为字符串集合
+ * @param str 字符串
+ * @return 分割后的字符串集合
+ */
+ public default List convertStringToList(String str) {
+ return Arrays.asList(str.split(","));
+ }
+
+ /**
+ * [default] 生成授权码
+ * @param clientId 应用id
+ * @param scope 权限
+ * @param loginId 账号id
+ * @return 授权码
+ */
+ public default String createCode(String clientId, String scope, Object loginId) {
+ return SaTokenInsideUtil.getRandomString(60).toLowerCase();
+ }
+
+ /**
+ * [default] 生成AccessToken
+ * @param codeModel CodeModel对象
+ * @return AccessToken
+ */
+ public default String createAccessToken(CodeModel codeModel) {
+ return SaTokenInsideUtil.getRandomString(60).toLowerCase();
+ }
+
+ /**
+ * [default] 生成RefreshToken
+ * @param codeModel CodeModel对象
+ * @return RefreshToken
+ */
+ public default String createRefreshToken(CodeModel codeModel) {
+ return SaTokenInsideUtil.getRandomString(60).toLowerCase();
+ }
+
+ /**
+ * [default] 在url上拼接上kv参数并返回
+ * @param url url
+ * @param parameStr 参数, 例如 id=1001
+ * @return 拼接后的url字符串
+ */
+ public default String splicingParame(String url, String parameStr) {
+ // 如果参数为空, 直接返回
+ if(parameStr == null || parameStr.length() == 0) {
+ return url;
+ }
+ int index = url.indexOf('?');
+ // ? 不存在
+ if(index == -1) {
+ return url + '?' + parameStr;
+ }
+ // ? 是最后一位
+ if(index == url.length() - 1) {
+ return url + parameStr;
+ }
+ // ? 是其中一位
+ if(index > -1 && index < url.length() - 1) {
+ String separatorChar = "&";
+ // 如果最后一位是 不是&, 且 arg_str 第一位不是 &, 就增送一个 &
+ if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) {
+ return url + separatorChar + parameStr;
+ } else {
+ return url + parameStr;
+ }
+ }
+ // 正常情况下, 代码不可能执行到此
+ return url;
+ }
+
+ /**
+ * [default] 将 CodeModel 转换为 AccessTokenModel
+ * @param codeModel CodeModel对象
+ * @return AccessToken对象
+ */
+ public default AccessTokenModel converCodeToAccessToken(CodeModel codeModel) {
+ if(codeModel == null) {
+ throw new SaTokenException("无效code");
+ }
+ AccessTokenModel tokenModel = new AccessTokenModel();
+ tokenModel.setAccessToken(createAccessToken(codeModel));
+ tokenModel.setRefreshToken(createRefreshToken(codeModel));
+ tokenModel.setCode(codeModel.getCode());
+ tokenModel.setClientId(codeModel.getClientId());
+ tokenModel.setScope(codeModel.getScope());
+ tokenModel.setOpenid(getOpenid(codeModel.getClientId(), codeModel.getLoginId()));
+ tokenModel.setTag(codeModel.getTag());
+ return tokenModel;
+ }
+
+
+ // ------------------- 返回相应key
+
+ /**
+ * 获取key:授权码持久化使用的key
+ * @param code 授权码
+ * @return key
+ */
+ public default String getKeyCodeModel(String code) {
+ return SaTokenManager.getConfig().getTokenName() + ":oauth2:code:" + code;
+ }
+
+ /**
+ * 获取key:[Client&账号]最新授权码记录, 持久化使用的key
+ * @param loginId 账号id
+ * @param clientId 应用id
+ * @return key
+ */
+ public default String getKeyClientLoginId(Object loginId, String clientId) {
+ return SaTokenManager.getConfig().getTokenName() + ":oauth2:newest-code:" + clientId + ":" + loginId;
+ }
+
+ /**
+ * 获取key:refreshToken持久化使用的key
+ * @param refreshToken refreshToken
+ * @return key
+ */
+ public default String getKeyRefreshToken(String refreshToken) {
+ return SaTokenManager.getConfig().getTokenName() + ":oauth2:refresh-token:" + refreshToken;
+ }
+
+ /**
+ * 获取key:accessToken持久化使用的key
+ * @param accessToken accessToken
+ * @return key
+ */
+ public default String getKeyAccessToken(String accessToken) {
+ return SaTokenManager.getConfig().getTokenName() + ":oauth2:access-token:" + accessToken;
+ }
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2InterfaceDefaultImpl.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2InterfaceDefaultImpl.java
new file mode 100644
index 00000000..20d23790
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2InterfaceDefaultImpl.java
@@ -0,0 +1,12 @@
+package cn.dev33.satoken.oauth2.logic;
+
+/**
+ * SaOAuth2Interface 默认实现类 (只构建userinfo单个权限)
+ * @author kong
+ *
+ */
+public class SaOAuth2InterfaceDefaultImpl implements SaOAuth2Interface {
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java
new file mode 100644
index 00000000..2c5ecbcc
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java
@@ -0,0 +1,197 @@
+package cn.dev33.satoken.oauth2.logic;
+
+import java.util.List;
+
+import cn.dev33.satoken.oauth2.SaOAuth2Manager;
+import cn.dev33.satoken.oauth2.model.AccessTokenModel;
+import cn.dev33.satoken.oauth2.model.CodeModel;
+import cn.dev33.satoken.oauth2.model.RequestAuthModel;
+
+/**
+ * sa-token-oauth2 模块 静态类接口转发, 方便调用
+ * @author kong
+ *
+ */
+public class SaOAuth2Util {
+
+ // ------------------- 获取数据
+
+ /**
+ * 返回此平台所有权限集合
+ * @return 此平台所有权限名称集合
+ */
+ public static List getAppScopeList() {
+ return SaOAuth2Manager.getInterface().getAppScopeList();
+ }
+
+ /**
+ * 返回指定Client签约的所有Scope名称集合
+ * @param clientId 应用id
+ * @return Scope集合
+ */
+ public static List getClientScopeList(String clientId) {
+ return SaOAuth2Manager.getInterface().getClientScopeList(clientId);
+ }
+
+ /**
+ * 获取指定 LoginId 对指定 Client 已经授权过的所有 Scope
+ * @param clientId 应用id
+ * @param loginId 账号id
+ * @return Scope集合
+ */
+ public static List getGrantScopeList(Object loginId, String clientId) {
+ return SaOAuth2Manager.getInterface().getGrantScopeList(loginId, clientId);
+ }
+
+
+ // ------------------- 数据校验
+
+ /**
+ * 指定 loginId 是否对一个 Client 授权给了指定 Scope
+ * @param clientId 应用id
+ * @param scope 权限
+ * @param loginId 账号id
+ */
+ public static boolean isGrant(Object loginId, String clientId, String scope) {
+ return SaOAuth2Manager.getInterface().isGrant(loginId, clientId, scope);
+ }
+
+ /**
+ * 校验code、clientId、clientSecret 三者是否正确
+ * @param code 授权码
+ * @param clientId 应用id
+ * @param clientSecret 秘钥
+ * @return CodeModel对象
+ */
+ public static CodeModel checkCodeIdSecret(String code, String clientId, String clientSecret) {
+ return SaOAuth2Manager.getInterface().checkCodeIdSecret(code, clientId, clientSecret);
+ }
+
+ /**
+ * [default] 校验access_token、clientId、clientSecret 三者是否正确
+ * @param accessToken access_token
+ * @param clientId 应用id
+ * @param clientSecret 秘钥
+ * @return AccessTokenModel对象
+ */
+ public static AccessTokenModel checkTokenIdSecret(String accessToken, String clientId, String clientSecret) {
+ return SaOAuth2Manager.getInterface().checkTokenIdSecret(accessToken, clientId, clientSecret);
+ }
+
+
+
+ // ------------------- 逻辑相关
+
+ /**
+ * 根据参数生成一个授权码并返回
+ * @param authModel 请求授权参数Model
+ * @return 授权码Model
+ */
+ public static CodeModel generateCode(RequestAuthModel authModel) {
+ return SaOAuth2Manager.getInterface().generateCode(authModel);
+ }
+
+ /**
+ * 根据授权码获得授权码Model
+ * @param code 授权码
+ * @return 授权码Model
+ */
+ public static CodeModel getCode(String code) {
+ return SaOAuth2Manager.getInterface().getCode(code);
+ }
+
+ /**
+ * 手动更改授权码对象信息
+ * @param code 授权码
+ * @param codeModel 授权码Model
+ */
+ public static void updateCode(String code, CodeModel codeModel) {
+ SaOAuth2Manager.getInterface().updateCode(code, codeModel);
+ }
+
+ /**
+ * 确认授权一个code
+ * @param code 授权码
+ */
+ public static void confirmCode(String code) {
+ SaOAuth2Manager.getInterface().confirmCode(code);
+ }
+
+ /**
+ * [default] 删除一个授权码
+ * @param code 授权码
+ */
+ public static void deleteCode(String code) {
+ SaOAuth2Manager.getInterface().deleteCode(code);
+ }
+
+ /**
+ * [default] 根据授权码Model生成一个access_token
+ * @param codeModel 授权码Model
+ * @return AccessTokenModel
+ */
+ public static AccessTokenModel generateAccessToken(CodeModel codeModel) {
+ return SaOAuth2Manager.getInterface().generateAccessToken(codeModel);
+ }
+
+ /**
+ * [default] 根据 access_token 获得其Model详细信息
+ * @param accessToken access_token
+ * @return AccessTokenModel (授权码Model)
+ */
+ public static AccessTokenModel getAccessToken(String accessToken) {
+ return SaOAuth2Manager.getInterface().getAccessToken(accessToken);
+ }
+
+ /**
+ * 根据 refresh_token 生成一个新的 access_token
+ * @param refreshToken refresh_token
+ * @return 新的 access_token
+ */
+ public static AccessTokenModel refreshAccessToken(String refreshToken) {
+ return SaOAuth2Manager.getInterface().refreshAccessToken(refreshToken);
+ }
+
+ /**
+ * [default] 根据 refresh_token 获得其Model详细信息 (授权码Model)
+ * @param refreshToken refresh_token
+ * @return RefreshToken (授权码Model)
+ */
+ public static CodeModel getRefreshToken(String refreshToken) {
+ return SaOAuth2Manager.getInterface().getRefreshToken(refreshToken);
+ }
+
+ /**
+ * [default] 获取 access_token 的有效期
+ * @param accessToken access_token
+ * @return 有效期
+ */
+ public static long getAccessTokenExpiresIn(String accessToken) {
+ return SaOAuth2Manager.getInterface().getAccessTokenExpiresIn(accessToken);
+ }
+
+ /**
+ * [default] 获取 refresh_token 的有效期
+ * @param refreshToken refresh_token
+ * @return 有效期
+ */
+ public static long getRefreshTokenExpiresIn(String refreshToken) {
+ return SaOAuth2Manager.getInterface().getRefreshTokenExpiresIn(refreshToken);
+ }
+
+ /**
+ * [default] 获取 access_token 所代表的LoginId
+ * @param accessToken access_token
+ * @return LoginId
+ */
+ public static Object getLoginIdByAccessToken(String accessToken) {
+ return SaOAuth2Manager.getInterface().getLoginIdByAccessToken(accessToken);
+ }
+
+
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java
new file mode 100644
index 00000000..21a36fa9
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java
@@ -0,0 +1,186 @@
+package cn.dev33.satoken.oauth2.model;
+
+/**
+ * Model: access_token
+ * @author kong
+ *
+ */
+public class AccessTokenModel {
+
+ /**
+ * access_token 值
+ */
+ private String accessToken;
+
+ /**
+ * refresh_token 值
+ */
+ private String refreshToken;
+
+ /**
+ * access_token 剩余有效时间 (秒)
+ */
+ private long expiresIn;
+
+ /**
+ * refresh_token 剩余有效期 (秒)
+ */
+ private long refreshExpiresIn;
+
+ /**
+ * 此 access_token令牌 是由哪个code码创建
+ */
+ private String code;
+
+ /**
+ * 应用id
+ */
+ private String clientId;
+
+ /**
+ * 授权范围
+ */
+ private String scope;
+
+ /**
+ * 开放账号id
+ */
+ private String openid;
+
+ /**
+ * 其他自定义数据
+ */
+ private Object tag;
+
+
+ /**
+ * @return accessToken
+ */
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ /**
+ * @param accessToken 要设置的 accessToken
+ */
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ /**
+ * @return refreshToken
+ */
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ /**
+ * @param refreshToken 要设置的 refreshToken
+ */
+ public void setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ /**
+ * @return expiresIn
+ */
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
+ /**
+ * @param expiresIn 要设置的 expiresIn
+ */
+ public void setExpiresIn(long expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+
+ /**
+ * @return refreshExpiresIn
+ */
+ public long getRefreshExpiresIn() {
+ return refreshExpiresIn;
+ }
+
+ /**
+ * @param refreshExpiresIn 要设置的 refreshExpiresIn
+ */
+ public void setRefreshExpiresIn(long refreshExpiresIn) {
+ this.refreshExpiresIn = refreshExpiresIn;
+ }
+
+ /**
+ * @return code
+ */
+ public String getCode() {
+ return code;
+ }
+
+ /**
+ * @param code 要设置的 code
+ */
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ /**
+ * @return clientId
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * @param clientId 要设置的 clientId
+ */
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ /**
+ * @return scope
+ */
+ public String getScope() {
+ return scope;
+ }
+
+ /**
+ * @param scope 要设置的 scope
+ */
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ /**
+ * @return openid
+ */
+ public String getOpenid() {
+ return openid;
+ }
+
+ /**
+ * @param openid 要设置的 openid
+ */
+ public void setOpenid(String openid) {
+ this.openid = openid;
+ }
+
+ /**
+ * @return tag
+ */
+ public Object getTag() {
+ return tag;
+ }
+
+ /**
+ * @param tag 要设置的 tag
+ */
+ public void setTag(Object tag) {
+ this.tag = tag;
+ }
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java
new file mode 100644
index 00000000..5600606d
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java
@@ -0,0 +1,191 @@
+package cn.dev33.satoken.oauth2.model;
+
+/**
+ * Model: [授权码 - 数据 对应关系]
+ * @author kong
+ *
+ */
+public class CodeModel {
+
+ /**
+ * 授权码
+ */
+ private String code;
+
+ /**
+ * 应用id
+ */
+ private String clientId;
+
+ /**
+ * 授权范围
+ */
+ private String scope;
+
+ /**
+ * 对应账号id
+ */
+ private Object loginId;
+
+ /**
+ * 用户是否已经确认了这个授权
+ */
+ private Boolean isConfirm;
+
+ /**
+ * 确认授权后重定向的地址
+ */
+ private String redirectUri;
+
+ /**
+ * 拒绝授权后重定向的地址
+ */
+ private String rejectUri;
+
+
+ /**
+ * 其他自定义数据
+ */
+ private Object tag;
+
+
+ /**
+ * 构建一个
+ */
+ public CodeModel() {
+
+ }
+ /**
+ * 构建一个
+ * @param code 授权码
+ * @param clientId 应用id
+ * @param scope 请求授权范围
+ * @param loginId 对应的账号id
+ */
+ public CodeModel(String code, String clientId, String scope, Object loginId) {
+ super();
+ this.code = code;
+ this.clientId = clientId;
+ this.scope = scope;
+ this.loginId = loginId;
+ this.isConfirm = false;
+ }
+
+
+
+ /**
+ * @return code
+ */
+ public String getCode() {
+ return code;
+ }
+
+ /**
+ * @param code 要设置的 code
+ */
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ /**
+ * @return clientId
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * @param clientId 要设置的 clientId
+ */
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ /**
+ * @return scope
+ */
+ public String getScope() {
+ return scope;
+ }
+
+ /**
+ * @param scope 要设置的 scope
+ */
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ /**
+ * @return loginId
+ */
+ public Object getLoginId() {
+ return loginId;
+ }
+
+ /**
+ * @param loginId 要设置的 loginId
+ */
+ public void setLoginId(Object loginId) {
+ this.loginId = loginId;
+ }
+
+ /**
+ * @return isConfirm
+ */
+ public Boolean getIsConfirm() {
+ return isConfirm;
+ }
+
+ /**
+ * @param isConfirm 要设置的 isConfirm
+ */
+ public void setIsConfirm(Boolean isConfirm) {
+ this.isConfirm = isConfirm;
+ }
+
+ /**
+ * @return redirectUri
+ */
+ public String getRedirectUri() {
+ return redirectUri;
+ }
+
+ /**
+ * @param redirectUri 要设置的 redirectUri
+ */
+ public void setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ /**
+ * @return rejectUri
+ */
+ public String getRejectUri() {
+ return rejectUri;
+ }
+ /**
+ * @param rejectUri 要设置的 rejectUri
+ */
+ public void setRejectUri(String rejectUri) {
+ this.rejectUri = rejectUri;
+ }
+
+ /**
+ * @return tag
+ */
+ public Object getTag() {
+ return tag;
+ }
+
+ /**
+ * @param tag 要设置的 tag
+ */
+ public void setTag(Object tag) {
+ this.tag = tag;
+ }
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java
new file mode 100644
index 00000000..4b30bfb0
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java
@@ -0,0 +1,153 @@
+package cn.dev33.satoken.oauth2.model;
+
+import cn.dev33.satoken.exception.SaTokenException;
+import cn.dev33.satoken.util.SaTokenInsideUtil;
+
+/**
+ * 请求授权参数的Model
+ * @author kong
+ *
+ */
+public class RequestAuthModel {
+
+ /**
+ * 应用id
+ */
+ private String clientId;
+
+ /**
+ * 授权范围
+ */
+ private String scope;
+
+ /**
+ * 对应的账号id
+ */
+ private Object loginId;
+
+ /**
+ * 待重定向URL
+ */
+ private String redirectUri;
+
+ /**
+ * 授权类型, 非必填
+ */
+ private String responseType;
+
+ /**
+ * 状态标识, 可为null
+ */
+ private String state;
+
+
+ /**
+ * @return clientId
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * @param clientId 要设置的 clientId
+ */
+ public RequestAuthModel setClientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ /**
+ * @return scope
+ */
+ public String getScope() {
+ return scope;
+ }
+
+ /**
+ * @param scope 要设置的 scope
+ */
+ public RequestAuthModel setScope(String scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ /**
+ * @return loginId
+ */
+ public Object getLoginId() {
+ return loginId;
+ }
+
+ /**
+ * @param loginId 要设置的 loginId
+ */
+ public RequestAuthModel setLoginId(Object loginId) {
+ this.loginId = loginId;
+ return this;
+ }
+
+ /**
+ * @return redirectUri
+ */
+ public String getRedirectUri() {
+ return redirectUri;
+ }
+
+ /**
+ * @param redirectUri 要设置的 redirectUri
+ */
+ public RequestAuthModel setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ return this;
+ }
+
+ /**
+ * @return responseType
+ */
+ public String getResponseType() {
+ return responseType;
+ }
+
+ /**
+ * @param responseType 要设置的 responseType
+ */
+ public RequestAuthModel setResponseType(String responseType) {
+ this.responseType = responseType;
+ return this;
+ }
+
+ /**
+ * @return state
+ */
+ public String getState() {
+ return state;
+ }
+
+ /**
+ * @param state 要设置的 state
+ */
+ public RequestAuthModel setState(String state) {
+ this.state = state;
+ return this;
+ }
+
+ /**
+ * 检查此Model参数是否有效
+ */
+ public RequestAuthModel checkModel() {
+ if(SaTokenInsideUtil.isEmpty(clientId)) {
+ throw new SaTokenException("无效client_id");
+ }
+ if(SaTokenInsideUtil.isEmpty(scope)) {
+ throw new SaTokenException("无效scope");
+ }
+ if(SaTokenInsideUtil.isEmpty(redirectUri)) {
+ throw new SaTokenException("无效redirect_uri");
+ }
+ if(SaTokenInsideUtil.isEmpty(String.valueOf(loginId))) {
+ throw new SaTokenException("无效LoginId");
+ }
+ return this;
+ }
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ScopeModel.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ScopeModel.java
new file mode 100644
index 00000000..a994eee2
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ScopeModel.java
@@ -0,0 +1,73 @@
+package cn.dev33.satoken.oauth2.model;
+
+/**
+ * 权限Model
+ * @author kong
+ *
+ */
+public class ScopeModel {
+
+ /**
+ * 权限名称
+ */
+ private String name;
+
+ /**
+ * 详细介绍
+ */
+ private String introduce;
+
+
+ /**
+ * 构造一个
+ */
+ public ScopeModel() {
+ super();
+ }
+ /**
+ * 构造一个
+ * @param id 权限id
+ * @param introduce 权限详细介绍
+ */
+ public ScopeModel(String name, String introduce) {
+ super();
+ this.name = name;
+ this.introduce = introduce;
+ }
+
+ /**
+ * @return name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name 要设置的 name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return introduce
+ */
+ public String getIntroduce() {
+ return introduce;
+ }
+
+ /**
+ * @param introduce 要设置的 introduce
+ */
+ public void setIntroduce(String introduce) {
+ this.introduce = introduce;
+ }
+
+
+
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2Consts.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2Consts.java
new file mode 100644
index 00000000..129bb2f2
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2Consts.java
@@ -0,0 +1,19 @@
+package cn.dev33.satoken.oauth2.util;
+
+/**
+ * sa-token oauth2 模块 用到的所有常量
+ * @author kong
+ *
+ */
+public class SaOAuth2Consts {
+
+ /**
+ * 在保存授权码时用到的key
+ */
+ public static final String UNLIMITED_DOMAIN = "*";
+
+
+
+
+
+}
diff --git a/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2InsideUtil.java b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2InsideUtil.java
new file mode 100644
index 00000000..fdd3b6d0
--- /dev/null
+++ b/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/util/SaOAuth2InsideUtil.java
@@ -0,0 +1,28 @@
+package cn.dev33.satoken.oauth2.util;
+
+/**
+ * sa-token-oauth2 模块内部算法util
+ * @author kong
+ *
+ */
+public class SaOAuth2InsideUtil {
+
+ /**
+ * 验证URL的正则表达式
+ */
+ static final String URL_REGEX = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]";
+
+ /**
+ * 使用正则表达式判断一个字符串是否为URL
+ * @param str 字符串
+ * @return 拼接后的url字符串
+ */
+ public static boolean isUrl(String str) {
+ if(str == null) {
+ return false;
+ }
+ return str.toLowerCase().matches(URL_REGEX);
+ }
+
+
+}