feat: add h5 account auth flow
This commit is contained in:
1 parent
ef6346a563
commit
e4e3f0fdb3
13 files changed
+774
-51
No files matched your search
@@ -2,20 +2,29 @@ package xuan.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xuan.entity.UserInfo;
|
||||
import xuan.service.UserInfoService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@@ -30,6 +39,15 @@ public class UserController {
|
||||
|
||||
@Value("${wechat.secret:}")
|
||||
private String appSecret;
|
||||
|
||||
@Value("${wechat.web.appid:}")
|
||||
private String webAppId;
|
||||
|
||||
@Value("${wechat.web.secret:}")
|
||||
private String webAppSecret;
|
||||
|
||||
@Value("${wechat.web.callback-base-url:}")
|
||||
private String webCallbackBaseUrl;
|
||||
|
||||
@PostMapping("/login")
|
||||
public Map<String, Object> login(@RequestBody Map<String, Object> params) {
|
||||
@@ -90,23 +108,10 @@ public class UserController {
|
||||
}
|
||||
|
||||
UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
|
||||
|
||||
System.out.println("登录成功: userId=" + user.getUserId() + ", openId=" + user.getOpenId());
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("id", user.getId());
|
||||
data.put("userId", user.getUserId());
|
||||
data.put("openId", user.getOpenId());
|
||||
data.put("avatarUrl", user.getAvatarUrl());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("vipLevel", user.getVipLevel());
|
||||
data.put("coins", user.getCoins());
|
||||
data.put("streakDays", user.getStreakDays());
|
||||
data.put("createTime", user.getCreateTime());
|
||||
|
||||
|
||||
result.put("success", true);
|
||||
result.put("data", data);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
System.err.println("登录异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
@@ -115,6 +120,117 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/wechat/h5/start")
|
||||
public ResponseEntity<Void> startWechatH5Login(
|
||||
@RequestParam(required = false) String returnUrl,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
String targetReturnUrl = StringUtils.hasText(returnUrl)
|
||||
? returnUrl
|
||||
: resolveDefaultFrontendReturnUrl(request);
|
||||
|
||||
if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
|
||||
}
|
||||
|
||||
String callbackBaseUrl = StringUtils.hasText(webCallbackBaseUrl)
|
||||
? webCallbackBaseUrl
|
||||
: resolveRequestBaseUrl(request);
|
||||
|
||||
String callbackUrl = UriComponentsBuilder
|
||||
.fromHttpUrl(callbackBaseUrl + "/api/user/wechat/h5/callback")
|
||||
.queryParam("returnUrl", targetReturnUrl)
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
String wechatLoginUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://open.weixin.qq.com/connect/qrconnect")
|
||||
.queryParam("appid", webAppId)
|
||||
.queryParam("redirect_uri", callbackUrl)
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("scope", "snsapi_login")
|
||||
.queryParam("state", "xuan_h5_login")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString() + "#wechat_redirect";
|
||||
|
||||
return redirect(wechatLoginUrl);
|
||||
}
|
||||
|
||||
@GetMapping("/wechat/h5/callback")
|
||||
public ResponseEntity<Void> handleWechatH5Callback(
|
||||
@RequestParam(required = false) String code,
|
||||
@RequestParam(required = false) String returnUrl
|
||||
) {
|
||||
String targetReturnUrl = StringUtils.hasText(returnUrl)
|
||||
? returnUrl
|
||||
: "/";
|
||||
|
||||
if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(code)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未获取到微信登录凭证"));
|
||||
}
|
||||
|
||||
try {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
String accessTokenUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://api.weixin.qq.com/sns/oauth2/access_token")
|
||||
.queryParam("appid", webAppId)
|
||||
.queryParam("secret", webAppSecret)
|
||||
.queryParam("code", code)
|
||||
.queryParam("grant_type", "authorization_code")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> tokenResult = restTemplate.getForObject(accessTokenUrl, Map.class);
|
||||
if (tokenResult == null || !tokenResult.containsKey("openid") || !tokenResult.containsKey("access_token")) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, extractWechatError(tokenResult, "微信扫码登录失败")));
|
||||
}
|
||||
|
||||
String openId = String.valueOf(tokenResult.get("openid"));
|
||||
String accessToken = String.valueOf(tokenResult.get("access_token"));
|
||||
|
||||
String username = "微信用户";
|
||||
String avatarUrl = "";
|
||||
String userInfoUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://api.weixin.qq.com/sns/userinfo")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("openid", openId)
|
||||
.queryParam("lang", "zh_CN")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> userInfoResult = restTemplate.getForObject(userInfoUrl, Map.class);
|
||||
if (userInfoResult != null && !userInfoResult.containsKey("errcode")) {
|
||||
if (userInfoResult.get("nickname") != null) {
|
||||
username = String.valueOf(userInfoResult.get("nickname"));
|
||||
}
|
||||
if (userInfoResult.get("headimgurl") != null) {
|
||||
avatarUrl = String.valueOf(userInfoResult.get("headimgurl"));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取微信用户信息失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, true, null, buildUserData(user)));
|
||||
} catch (Exception e) {
|
||||
System.err.println("H5微信扫码回调异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "微信扫码登录失败,请重试"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
public Map<String, Object> getUserInfo(@RequestParam String userId) {
|
||||
@@ -146,6 +262,41 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/h5/register")
|
||||
public Map<String, Object> registerH5(@RequestBody Map<String, Object> params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
|
||||
String nickname = params.get("nickname") != null ? params.get("nickname").toString() : "";
|
||||
String password = params.get("password") != null ? params.get("password").toString() : "";
|
||||
|
||||
UserInfo user = userInfoService.registerWithPassword(loginName, nickname, password);
|
||||
result.put("success", true);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/h5/login")
|
||||
public Map<String, Object> loginH5(@RequestBody Map<String, Object> params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
|
||||
String password = params.get("password") != null ? params.get("password").toString() : "";
|
||||
|
||||
UserInfo user = userInfoService.loginWithPassword(loginName, password);
|
||||
result.put("success", true);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/updateVip")
|
||||
public Map<String, Object> updateVipLevel(@RequestBody Map<String, Object> params) {
|
||||
@@ -407,4 +558,109 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildUserData(UserInfo user) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("id", user.getId());
|
||||
data.put("userId", user.getUserId());
|
||||
data.put("openId", user.getOpenId());
|
||||
data.put("avatarUrl", user.getAvatarUrl());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("loginName", user.getLoginName());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("vipLevel", user.getVipLevel());
|
||||
data.put("coins", user.getCoins());
|
||||
data.put("streakDays", user.getStreakDays());
|
||||
data.put("createTime", user.getCreateTime());
|
||||
return data;
|
||||
}
|
||||
|
||||
private ResponseEntity<Void> redirect(String targetUrl) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setLocation(URI.create(targetUrl));
|
||||
return new ResponseEntity<>(headers, HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
private String buildFrontendRedirectUrl(String returnUrl, boolean success, String errorMessage) {
|
||||
return buildFrontendRedirectUrl(returnUrl, success, errorMessage, null);
|
||||
}
|
||||
|
||||
private String buildFrontendRedirectUrl(
|
||||
String returnUrl,
|
||||
boolean success,
|
||||
String errorMessage,
|
||||
Map<String, Object> payload
|
||||
) {
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("success", success ? "1" : "0");
|
||||
if (!success && StringUtils.hasText(errorMessage)) {
|
||||
params.put("error", errorMessage);
|
||||
}
|
||||
if (payload != null) {
|
||||
params.putAll(payload);
|
||||
}
|
||||
return appendParamsToUrl(returnUrl, params);
|
||||
}
|
||||
|
||||
private String appendParamsToUrl(String url, Map<String, Object> params) {
|
||||
if (!StringUtils.hasText(url)) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
int hashIndex = url.indexOf('#');
|
||||
if (hashIndex >= 0) {
|
||||
String prefix = url.substring(0, hashIndex);
|
||||
String fragment = url.substring(hashIndex + 1);
|
||||
String separator = fragment.contains("?") ? "&" : "?";
|
||||
return prefix + "#" + fragment + separator + toQueryString(params);
|
||||
}
|
||||
|
||||
String separator = url.contains("?") ? "&" : "?";
|
||||
return url + separator + toQueryString(params);
|
||||
}
|
||||
|
||||
private String toQueryString(Map<String, Object> params) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append("&");
|
||||
}
|
||||
builder
|
||||
.append(urlEncode(entry.getKey()))
|
||||
.append("=")
|
||||
.append(urlEncode(String.valueOf(entry.getValue())));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String urlEncode(String value) {
|
||||
try {
|
||||
return URLEncoder.encode(value, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("URL编码失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractWechatError(Map<String, Object> wxResult, String fallback) {
|
||||
if (wxResult == null) {
|
||||
return fallback;
|
||||
}
|
||||
Object errMsg = wxResult.get("errmsg");
|
||||
if (errMsg != null && StringUtils.hasText(String.valueOf(errMsg))) {
|
||||
return String.valueOf(errMsg);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String resolveRequestBaseUrl(HttpServletRequest request) {
|
||||
return request.getScheme() + "://" + request.getServerName()
|
||||
+ (request.getServerPort() == 80 || request.getServerPort() == 443 ? "" : ":" + request.getServerPort());
|
||||
}
|
||||
|
||||
private String resolveDefaultFrontendReturnUrl(HttpServletRequest request) {
|
||||
return resolveRequestBaseUrl(request) + "/#/pages/auth/wechat-callback";
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,12 @@ public class UserInfo {
|
||||
|
||||
@Column(length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(unique = true, length = 50)
|
||||
private String loginName;
|
||||
|
||||
@Column(length = 64)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(length = 20)
|
||||
private String userId;
|
||||
@@ -65,6 +71,12 @@ public class UserInfo {
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getLoginName() { return loginName; }
|
||||
public void setLoginName(String loginName) { this.loginName = loginName; }
|
||||
|
||||
public String getPasswordHash() { return passwordHash; }
|
||||
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
@@ -8,4 +8,5 @@ import xuan.entity.UserInfo;
|
||||
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
|
||||
UserInfo findByOpenId(String openId);
|
||||
UserInfo findByUserId(String userId);
|
||||
UserInfo findByLoginName(String loginName);
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import xuan.entity.CoinConsumeRecord;
|
||||
import xuan.repository.UserInfoRepository;
|
||||
import xuan.repository.CoinConsumeRecordRepository;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
@@ -40,6 +42,53 @@ public class UserInfoService {
|
||||
|
||||
return userInfoRepository.save(user);
|
||||
}
|
||||
|
||||
public UserInfo registerWithPassword(String loginName, String nickname, String password) {
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入用户名");
|
||||
}
|
||||
if (nickname == null || nickname.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入昵称");
|
||||
}
|
||||
if (password == null || password.length() < 6) {
|
||||
throw new IllegalArgumentException("密码至少6位");
|
||||
}
|
||||
|
||||
String normalizedLoginName = loginName.trim();
|
||||
if (userInfoRepository.findByLoginName(normalizedLoginName) != null) {
|
||||
throw new IllegalArgumentException("用户名已存在");
|
||||
}
|
||||
|
||||
UserInfo user = new UserInfo();
|
||||
user.setOpenId("h5_" + normalizedLoginName);
|
||||
user.setLoginName(normalizedLoginName);
|
||||
user.setPasswordHash(hashPassword(password));
|
||||
user.setUsername(nickname.trim());
|
||||
user.setAvatarUrl("");
|
||||
user.setUserId(generateUserId());
|
||||
user.setVipLevel(0);
|
||||
user.setStreakDays(0);
|
||||
|
||||
return userInfoRepository.save(user);
|
||||
}
|
||||
|
||||
public UserInfo loginWithPassword(String loginName, String password) {
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入用户名");
|
||||
}
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入密码");
|
||||
}
|
||||
|
||||
UserInfo user = userInfoRepository.findByLoginName(loginName.trim());
|
||||
if (user == null || user.getPasswordHash() == null) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
if (!user.getPasswordHash().equals(hashPassword(password))) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public UserInfo getUserInfo(String userId) {
|
||||
return userInfoRepository.findByUserId(userId);
|
||||
@@ -175,4 +224,18 @@ public class UserInfoService {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String hashPassword(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("密码处理失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ spring.datasource.password=xuan_password
|
||||
|
||||
# JPA Configuration
|
||||
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
@@ -17,5 +17,10 @@ spring.jpa.properties.hibernate.format_sql=true
|
||||
logging.level.xuan=DEBUG
|
||||
|
||||
# WeChat Mini Program Configuration
|
||||
wechat.appid=wxbae101ca6e1f61e6
|
||||
wechat.secret=26f414d93627acf3e1d895231296b313
|
||||
wechat.appid=${WECHAT_APPID:}
|
||||
wechat.secret=${WECHAT_SECRET:}
|
||||
|
||||
# WeChat H5 QR Login Configuration
|
||||
wechat.web.appid=${WECHAT_WEB_APPID:${WECHAT_APPID:}}
|
||||
wechat.web.secret=${WECHAT_WEB_SECRET:${WECHAT_SECRET:}}
|
||||
wechat.web.callback-base-url=${WECHAT_WEB_CALLBACK_BASE_URL:https://api.bemover.xyz}
|
||||
Reference in new issue
Block a user