Преглед на файлове

手机号一键登录(缺少短信服务)

ximinghao преди 3 месеца
родител
ревизия
d618fb0e50

+ 5 - 0
pom.xml

@@ -111,6 +111,11 @@
             <artifactId>jts-core</artifactId>
             <version>1.18.2</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.1-jre</version>
+        </dependency>
         <dependency>
             <groupId>com.github.desoss</groupId>
             <artifactId>jackson-datatype-jts</artifactId>

+ 27 - 3
src/main/java/com/skyversation/xjcy/controller/LoginController.java

@@ -2,7 +2,6 @@ package com.skyversation.xjcy.controller;
 
 
 import com.skyversation.xjcy.oauth.AuthService;
-import com.skyversation.xjcy.service.SerialNumberGenerator;
 import com.skyversation.xjcy.util.MessageManage;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
@@ -25,7 +24,7 @@ public class LoginController {
     }
 
     @RequestMapping(value = "/wechat", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
-    public String serialNumber(@RequestParam(required = false)String jsCode) {
+    public String byWechat(@RequestParam(required = false)String jsCode) {
         try {
             if (jsCode == null) {
                 return MessageManage.getInstance().getResultContent(-1,"缺失jsCode","缺失jsCode");
@@ -34,6 +33,31 @@ public class LoginController {
         } catch (Exception e) {
             return MessageManage.getInstance().getResultContent(500, e.getMessage(), "未知错误");
         }
-
+    }
+    @RequestMapping(value = "/phone")
+    public String byPhone(@RequestParam(required = false)String phone,@RequestParam(required = false)String code){
+        try {
+            if (phone == null||code==null) {
+                return MessageManage.getInstance().getResultContent(-1,"缺少参数","缺少参数");
+            }
+            return authService.logOrRegPhoneAccount(phone,code);
+        } catch (Exception e) {
+            return MessageManage.getInstance().getResultContent(500, e.getMessage(), "未知错误");
+        }
+    }
+    @RequestMapping(value = "/phone/sendCode")
+    public String sendPhoneCode(@RequestParam(required = false)String phone){
+        try {
+            if (phone == null) {
+                return MessageManage.getInstance().getResultContent(-1,"缺少参数","缺少参数");
+            }
+            boolean success = authService.sendPhoneCode(phone);
+            if (success) {
+                return MessageManage.getInstance().getResultContent(200, "成功","成功");
+            }
+            return MessageManage.getInstance().getResultContent(500, "未知错误", "未知错误");
+        } catch (Exception e) {
+            return MessageManage.getInstance().getResultContent(500, e.getMessage(), "未知错误");
+        }
     }
 }

+ 252 - 78
src/main/java/com/skyversation/xjcy/oauth/AuthService.java

@@ -4,75 +4,134 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.skyversation.xjcy.util.HttpUtil;
 import com.skyversation.xjcy.util.MessageManage;
+import lombok.Getter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 
+import javax.annotation.PostConstruct;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Objects;
 
+/**
+ * OAuth认证服务类
+ * 处理用户登录、注册、token管理等认证相关操作
+ */
 @Service
 public class AuthService {
 
+    // ============================ 常量定义 ============================
+
+    /** OAuth客户端ID */
+    private static final String CLIENT_ID = "2";
+    /** 成功状态码 */
+    private static final Integer SUCCESS_CODE_INT = 200;
+    /** 密码错误消息 */
+    private static final String MESSAGE_PASSWORD_ERROR = "密码错误";
+    /** 用户不存在消息 */
+    private static final String MESSAGE_USER_NOT_EXIST = "用户不存在";
+
+    /** 用户登录接口路径 */
+    private static final String API_USER_LOGIN = "/api/user/login";
+    /** 用户注册接口路径 */
+    private static final String USER_REGISTER = "/user/register";
+    /** Token验证接口路径 */
+    private static final String USER_VALIDATE_TOKEN = "/user/validateToken";
+
+    /** 微信账号用户名前缀 */
+    private static final String WX_NAME_PREFIX = "#wx";
+    /** 微信账号密码前缀 */
+    private static final String WX_PASSWORD_PREFIX = "Wx@";
+    /** 微信账号加密盐值 */
+    private static final String WX_SALT = "chatWe";
+    /** 手机账号用户名前缀 */
+    private static final String PHONE_NAME_PREFIX = "#phone";
+    /** 手机账号密码前缀 */
+    private static final String PHONE_PASSWORD_PREFIX = "Phone@";
+    /** 手机账号加密盐值 */
+    private static final String PHONE_SALT = "enohp";
+
+    /** 响应字段:状态码 */
+    private static final String RESPONSE_FIELD_CODE = "code";
+    /** 响应字段:消息 */
+    private static final String RESPONSE_FIELD_MESSAGE = "message";
+    /** 响应字段:内容数据 */
+    private static final String RESPONSE_FIELD_CONTENT = "content";
+    /** 响应字段:用户ID */
+    private static final String RESPONSE_FIELD_ID = "id";
+
+
+    // ============================ 内部类定义 ============================
+
+    /**
+     * 用户账户信息类
+     */
+    @Getter
+    public final static class Account {
+        private final String username;
+        private final String password;
+
+        public Account(String username, String password) {
+            if (username == null || password == null || username.isEmpty() || password.isEmpty()) {
+                throw new IllegalArgumentException("缺失必要参数");
+            }
+            this.username = username;
+            this.password = password;
+        }
+    }
+
+    // ============================ 实例变量 ============================
+
+    /** 微信认证服务 */
     private final WxAuthService wxAuthService;
+
+    private final PhoneService phoneService;
+    /** 服务账户用户名 */
     @Value("${app.oauth.login-name}")
     private String loginName;
+
+    /** 服务账户密码 */
     @Value("${app.oauth.password}")
     private String password;
+
+    /** 服务账户信息 */
+    private Account account;
+
+    /** OAuth服务路径 */
     @Value("${app.oauth.path}")
     private String oauthPath = null;
 
-    private String cacheToken;
+    /** Token缓存,使用volatile确保多线程可见性 */
+    private volatile String cacheToken;
 
+    // ============================ 构造方法 ============================
 
-    public AuthService(WxAuthService wxAuthService) {
+    public AuthService(WxAuthService wxAuthService, PhoneService phoneService) {
         this.wxAuthService = wxAuthService;
+        this.phoneService = phoneService;
     }
 
-    private String loginForToken(String loginName, String password) throws RuntimeException {
-        JSONObject jsonObject = login(loginName, password);
-        if ("密码错误".equals(jsonObject.getString("message"))) {
-            throw new RuntimeException("请检查使用的账户和密码是否正确");
-        }
-        if (!Objects.equals(jsonObject.getString("code"), "200")) {
-            throw new RuntimeException(jsonObject.getString("message"));
-        }
-        return jsonObject.getString("message");
-    }
+    // ============================ 初始化方法 ============================
 
-    private JSONObject login(String loginName, String password) {
+    /**
+     * 初始化方法 - 验证配置并创建服务账户
+     */
+    @PostConstruct
+    private void init() {
         if (!StringUtils.hasText(loginName) || !StringUtils.hasText(password)) {
-            throw new RuntimeException("请提供用于访问DMS的账号密码");
+            throw new IllegalStateException("OAuth配置不完整: login-name和password必须配置");
         }
-        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
-        params.add("clientId", "2");
-        params.add("userName", loginName);
-        params.add("password", password);
-        String response = HttpUtil.requestPost(oauthPath + "/api/user/login", params, new HashMap<>());
-        return JSON.parseObject(response);
-    }
-
-    private JSONObject register(String loginName, String password) {
-        if (!StringUtils.hasText(loginName) || !StringUtils.hasText(password)) {
-            throw new RuntimeException("请提供用于访问DMS的账号密码");
+        if (!StringUtils.hasText(oauthPath)) {
+            throw new IllegalStateException("OAuth配置不完整: oauth.path必须配置");
         }
-        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
-        params.add("username", loginName);
-        params.add("password", password);
-        Map<String, String> header = new HashMap<>();
-        header.put("token", getTokenOfServiceAccount());
-        String response = HttpUtil.requestPost(oauthPath + "/user/register", params, header);
-        return JSON.parseObject(response);
+        account = new Account(loginName, password);
     }
 
-    private void initUser(int userId) {
-        //TODO 初始化用户
-    }
+    // ============================ 公共方法 ============================
 
     /**
      * 检查token有效性
@@ -84,29 +143,34 @@ public class AuthService {
     public boolean checkToken(String token) {
         Map<String, String> header = new HashMap<>();
         header.put("token", token);
-        String response = HttpUtil.requestGet(oauthPath + "/user/validateToken", null, header);
+        String response = HttpUtil.requestGet(oauthPath + USER_VALIDATE_TOKEN, null, header);
         JSONObject jsonObject = JSON.parseObject(response);
-        Integer code = jsonObject.getInteger("code");
-        return Integer.valueOf(200).equals(code);
+        return isSuccess(jsonObject);
     }
 
     /**
      * 获取服务器内置的账户token
+     * 使用双重检查锁定确保线程安全
      *
      * @return 服务器内置账户的token
      * @throws RuntimeException 内置账户信息异常
      */
     public String getTokenOfServiceAccount() throws RuntimeException {
+        // 双重检查锁定,确保线程安全
         if (!StringUtils.hasText(cacheToken) || !checkToken(cacheToken)) {
-            cacheToken = loginForToken(this.loginName, this.password);
+            synchronized (this) {
+                if (!StringUtils.hasText(cacheToken) || !checkToken(cacheToken)) {
+                    cacheToken = loginForToken(account);
+                }
+            }
         }
         return cacheToken;
     }
 
     /**
-     * 通过wx的一键登录登录到oauth。
-     * 实际上每个wx用户都会被分配一个oauth账户,此账户以用户名与wx的openid建立连接。
-     * 逻辑上这个方法会尝试登录,无法登录时尝试建立oauth账户
+     * 通过微信一键登录登录到OAuth系统
+     * 每个微信用户都会被分配一个OAuth账户,用户名与微信openid关联
+     * 逻辑:尝试登录,无法登录时尝试建立OAuth账户
      *
      * @param wxCode wx.login()方法提供的一键登录用code
      * @return 完整的response.body
@@ -114,47 +178,158 @@ public class AuthService {
     public String logOrRegWxAccount(String wxCode) {
         WxAuthService.Result wxResult = wxAuthService.wxLogin(wxCode);
         if (wxResult.isSuccess()) {
-            //生成期望的绑定账户和密码
-            String oauthAccount = "#wx" + wxResult.getWxOpenId();
-            String oauthPassword = "Wx@" + encrypt(wxResult.getWxOpenId() + "chatWe");
-
-            //尝试登录
-            JSONObject result = login(oauthAccount, oauthPassword);
-            String message = result.getString("message");
-            Integer code = result.getInteger("code");
-            if (Integer.valueOf(200).equals(code)) {
-                return message;
-            } else {
-                //登录失败
-                if ("用户不存在".equals(message)) {
-                    //尝试注册
-                    JSONObject regResult = register(oauthAccount, oauthPassword);
-                    if (!Integer.valueOf(200).equals(regResult.getInteger("code"))) {
-                        return MessageManage.getInstance().getResultContent(-1, "注册失败,未知异常", "注册失败,未知异常");
-                    }
-                    JSONObject userUnInitResult = login(oauthAccount, oauthPassword);
-                    if (!Integer.valueOf(200).equals(userUnInitResult.getInteger("code"))) {
-                        return MessageManage.getInstance().getResultContent(-1, "注册失败,未知异常", "注册失败,未知异常");
-                    }
-                    Integer userId = userUnInitResult.getJSONObject("content").getInteger("id");
-                    if (userId != null) {
-                        initUser(userId);
-                    }
-                    return login(oauthAccount, oauthPassword).toJSONString();
-                } else {
-                    return MessageManage.getInstance().getResultContent(-1, "未知异常", "未知异常");
-                }
-            }
+            return logOrReg(generateAccount(wxResult.getWxOpenId(), WX_NAME_PREFIX, WX_PASSWORD_PREFIX, WX_SALT));
         } else {
             return MessageManage.getInstance().getResultContent(-1, wxResult.getError(), wxResult.getError());
         }
     }
 
+    /**
+     * 通过验证码策略一键登录登录到OAuth系统
+     * 每个用户都会被分配一个OAuth账户,用户名与手机号关联
+     * 逻辑:尝试登录,无法登录时尝试建立OAuth账户
+     *
+     * @param code phoneService提供的验证码
+     * @return 完整的response.body
+     */
+    public String logOrRegPhoneAccount(String phone,String code) {
+        PhoneService.Result result = phoneService.login(phone,code);
+        if (result.isSuccess()) {
+            return logOrReg(generateAccount(result.getPhone(), PHONE_NAME_PREFIX, PHONE_PASSWORD_PREFIX, PHONE_SALT));
+        } else {
+            return MessageManage.getInstance().getResultContent(-1, result.getError(), result.getError());
+        }
+    }
+
+
+    public boolean sendPhoneCode(String phone) {
+        return phoneService.sendCode(phone);
+    }
+
+    // ============================ 私有方法 ============================
 
     /**
-     * 简单sha-256加密,不加盐
+     * 登录并获取token
      */
-    public static String encrypt(String input) {
+    private String loginForToken(Account account) throws RuntimeException {
+        JSONObject jsonObject = login(account);
+        if (MESSAGE_PASSWORD_ERROR.equals(jsonObject.getString(RESPONSE_FIELD_MESSAGE))) {
+            throw new RuntimeException("请检查使用的账户和密码是否正确");
+        }
+        if (!isSuccess(jsonObject)) {
+            throw new RuntimeException(jsonObject.getString(RESPONSE_FIELD_MESSAGE));
+        }
+        return jsonObject.getString(RESPONSE_FIELD_MESSAGE);
+    }
+
+    /**
+     * 用户登录
+     */
+    private JSONObject login(Account account) {
+        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
+        params.add("clientId", CLIENT_ID);
+        params.add("userName", account.getUsername());
+        params.add("password", account.getPassword());
+        String response = HttpUtil.requestPost(oauthPath + API_USER_LOGIN, params, new HashMap<>());
+        return JSON.parseObject(response);
+    }
+
+    /**
+     * 用户注册
+     */
+    private JSONObject register(Account account) throws RuntimeException {
+        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
+        params.add("username", account.getUsername());
+        params.add("password", account.getPassword());
+        Map<String, String> header = new HashMap<>();
+        header.put("token", getTokenOfServiceAccount());
+        String response = HttpUtil.requestPost(oauthPath + USER_REGISTER, params, header);
+        return JSON.parseObject(response);
+    }
+
+    /**
+     * 初始化用户数据
+     */
+    private boolean initUser(int userId) {
+        //TODO 初始化用户
+        return true;
+    }
+
+    /**
+     * 登录或注册流程
+     */
+    private String logOrReg(Account account) {
+        // 尝试登录
+        JSONObject result = login(account);
+        String message = result.getString(RESPONSE_FIELD_MESSAGE);
+        boolean success = isSuccess(result);
+
+        if (success) {
+            return result.toJSONString();
+        } else {
+            // 登录失败处理
+            if (MESSAGE_USER_NOT_EXIST.equals(message)) {
+                boolean regSuccess = tryRegAndInit(account);
+                if (!regSuccess) {
+                    return MessageManage.getInstance().getResultContent(-1, "注册失败,未知异常", "注册失败,未知异常");
+                }
+                return login(account).toJSONString();
+            } else {
+                return MessageManage.getInstance().getResultContent(-1, "未知异常", "未知异常");
+            }
+        }
+    }
+
+    /**
+     * 尝试注册并初始化用户
+     */
+    private boolean tryRegAndInit(Account account) {
+        // 尝试注册
+        JSONObject regResult = register(account);
+        if (!isSuccess(regResult)) {
+            return false;
+        }
+
+        JSONObject userUnInitResult = login(account);
+        if (!isSuccess(userUnInitResult)) {
+            return false;
+        }
+
+        Integer userId = userUnInitResult.getJSONObject(RESPONSE_FIELD_CONTENT).getInteger(RESPONSE_FIELD_ID);
+        if (userId != null) {
+            return initUser(userId);
+        }
+        return false;
+    }
+
+    /**
+     * 检查响应是否成功
+     */
+    private static boolean isSuccess(JSONObject result) {
+        if (result == null) {
+            return false;
+        }
+        Integer code = result.getInteger(RESPONSE_FIELD_CODE);
+        return SUCCESS_CODE_INT.equals(code);
+    }
+
+    /**
+     * 生成账户信息
+     */
+    private static Account generateAccount(String uniCode, String namePrefix, String passwordPrefix, String salt) {
+        String oauthAccount = namePrefix + uniCode;
+        String oauthPassword = passwordPrefix + encrypt(uniCode + salt);
+        return new Account(oauthAccount, oauthPassword);
+    }
+
+
+    /**
+     * SHA-256加密方法
+     *
+     * @param input 待加密字符串
+     * @return 加密后的十六进制字符串
+     */
+    private static String encrypt(String input) {
         try {
             MessageDigest digest = MessageDigest.getInstance("SHA-256");
             byte[] hash = digest.digest(input.getBytes());
@@ -174,5 +349,4 @@ public class AuthService {
             throw new RuntimeException(e);
         }
     }
-
-}
+}

+ 81 - 0
src/main/java/com/skyversation/xjcy/oauth/PhoneService.java

@@ -0,0 +1,81 @@
+package com.skyversation.xjcy.oauth;
+
+import com.skyversation.xjcy.util.cache.ExpirableCache;
+import com.skyversation.xjcy.util.cache.UnifiedExpirationGuavaCacheImpl;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class PhoneService {
+
+    private final ConcurrentHashMap<String, Object> lockPool = new ConcurrentHashMap<>();
+
+    @Getter
+    @AllArgsConstructor
+    public static class Result {
+        private final boolean success;
+        private final String error;
+        private final String phone;
+    }
+
+    private ExpirableCache<String,String> cache;
+    @PostConstruct
+    public void init() {
+        cache = new UnifiedExpirationGuavaCacheImpl<>(
+                5,
+                TimeUnit.MINUTES,
+                null,
+                100000
+        );
+    }
+
+    public Result login(String phone, String code) {
+        if (checkCode(phone, code)) {
+            return new Result(true,null,phone);
+        }
+        return new Result(false,"验证码错误",null);
+    }
+
+    public boolean sendCode(String phone) {
+        String code = generateCode();
+        cache.put(phone,code,5,TimeUnit.MINUTES);
+        System.out.println(code);
+        return true;
+    }
+    private boolean checkCode(String phone,String code){
+        Object lock = lockPool.computeIfAbsent(phone, k -> new Object());
+
+        try {
+            synchronized (lock) {
+                String codeInCache = cache.get(phone);
+                if (Objects.equals(codeInCache, code)) {
+                    cache.remove(phone);
+                    return true;
+                }
+                return false;
+            }
+        }finally {
+            lockPool.remove(phone);
+        }
+    }
+    /**
+     * 生成指定长度的随机验证码
+     * 验证码由数字组成
+     *
+     * @return 返回4位随机验证码字符串
+     */
+    private static String generateCode() {
+        Random random = new Random();
+        // 生成0-9999的随机数
+        int randomNum = random.nextInt(10000);
+        // 格式化为4位数字,不足前面补0
+        return String.format("%04d", randomNum);
+    }
+}

+ 48 - 0
src/main/java/com/skyversation/xjcy/util/cache/ExpirableCache.java

@@ -0,0 +1,48 @@
+package com.skyversation.xjcy.util.cache;
+
+import java.util.concurrent.TimeUnit;
+
+public interface ExpirableCache<K, V> {
+
+    /**
+     * 将键值对存入缓存
+     *
+     * @param key 键
+     * @param value 值
+     * @param duration 过期时间长度
+     * @param timeUnit 过期时间单位
+     */
+    void put(K key, V value, long duration, TimeUnit timeUnit);
+
+    /**
+     * 根据键获取缓存值
+     *
+     * @param key 键
+     * @return 值,如果键不存在或已过期则返回null
+     */
+    V get(K key);
+
+    /**
+     * 原子性地获取并删除缓存项
+     * 此操作保证在并发环境下,一个缓存项只能被一个线程成功获取并删除
+     *
+     * @param key 键
+     * @return 值,如果键不存在或已过期则返回null
+     */
+    V getAndRemove(K key);
+
+    /**
+     * 显式删除缓存项
+     *
+     * @param key 要删除的键
+     */
+    void remove(K key);
+
+    /**
+     * 检查缓存中是否包含指定的键
+     *
+     * @param key 键
+     * @return 如果键存在且未过期则返回true
+     */
+    boolean containsKey(K key);
+}

+ 77 - 0
src/main/java/com/skyversation/xjcy/util/cache/UnifiedExpirationGuavaCacheImpl.java

@@ -0,0 +1,77 @@
+package com.skyversation.xjcy.util.cache;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class UnifiedExpirationGuavaCacheImpl<K, V> implements ExpirableCache<K, V> {
+
+    private final Cache<K, V> cache;
+
+    /**
+     * 构造函数,在创建缓存时指定统一的过期策略和过期时间
+     */
+    public UnifiedExpirationGuavaCacheImpl(long expireAfterWrite,
+                                           TimeUnit timeUnit,
+                                           Long expireAfterAccess,
+                                           long maxSize) {
+        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
+
+        if (expireAfterWrite > 0) {
+            builder.expireAfterWrite(expireAfterWrite, timeUnit);
+        }
+
+        if (expireAfterAccess != null && expireAfterAccess > 0) {
+            builder.expireAfterAccess(expireAfterAccess, timeUnit);
+        }
+
+        if (maxSize > 0) {
+            builder.maximumSize(maxSize);
+        }
+
+        this.cache = builder.build();
+    }
+
+    @Override
+    public void put(K key, V value, long duration, TimeUnit timeUnit) {
+        cache.put(key, value);
+    }
+
+    @Override
+    public V get(K key) {
+        return cache.getIfPresent(key);
+    }
+
+    @Override
+    public V getAndRemove(K key) {
+        // 使用原子操作获取并删除
+        V value = cache.getIfPresent(key);
+        if (value != null) {
+            cache.invalidate(key);
+        }
+        return value;
+    }
+
+    @Override
+    public void remove(K key) {
+        cache.invalidate(key);
+    }
+
+    @Override
+    public boolean containsKey(K key) {
+        return cache.getIfPresent(key) != null;
+    }
+
+    // 便捷构造方法
+    public static <K, V> UnifiedExpirationGuavaCacheImpl<K, V> createWithWriteExpiration(
+            long duration, TimeUnit unit, long maxSize) {
+        return new UnifiedExpirationGuavaCacheImpl<>(duration, unit, null , maxSize);
+    }
+
+    public static <K, V> UnifiedExpirationGuavaCacheImpl<K, V> createWithAccessExpiration(
+            long duration, TimeUnit unit, long maxSize) {
+        return new UnifiedExpirationGuavaCacheImpl<>(0, unit, duration, maxSize);
+    }
+}