小明网站微信登录改造记——OAuth2完整指南(含续期逻辑)

news/2025/11/26 10:03:27/文章来源:https://www.cnblogs.com/sun-10387834/p/19271340

一 、 故 事 背 景

小 明 运 营 着 一 个 电 商 网 站 , 用 户 需 要 登 录 才 能 购 物 。 之 前 他 用 Spring Security 实 现 了 账 号 密 码 登 录 , 但 随 着 竞 争 加 剧 , 用 户 嫌 注 册 麻 烦 流 失 严 重 。 为 了 提 升 用 户 体 验 , 小 明 决 定 引 入 微 信 登 录 功 能 , 让 用 户 一 键 授 权 即 可 登 录 。 这 就 涉 及 到 OAuth2 授 权 框 架 的 使 用 。

二 、 OAuth2 是 什 么

OAuth2 是 一 个 开 放 的 授 权 标 准 , 它 允 许 用 户 将 自 己 在 某 个 平 台 ( 如 微 信 ) 的 部 分 权 限 , 授 权 给 第 三 方 应 用 ( 如 小 明 的 网 站 ) 使 用 , 而 无 需 将 自 己 的 账 号 密 码 告 知 第 三 方 。

简 单 说 , OAuth2 解 决 的 是 “ 如 何 安 全 地 让 第 三 方 应 用 获 取 用 户 资 源 ” 的 问 题 。 比 如 微 信 登 录 时 , 小 明 的 网 站 并 不 会 获 取 用 户 的 微 信 账 号 密 码 , 而 是 通 过 微 信 授 权 服 务 器 获 取 一 个 临 时 令 牌 ( Token ) , 用 来 获 取 用 户 的 公 开 信 息 ( 如 昵 称 、 头 像 ) 。

三 、 OAuth2 使 用 整 体 流 程

OAuth2 有 四 种 授 权 模 式 , 微 信 登 录 采 用 ** 授 权 码 模 式 ** 。 我 们 用 “ 委 托 取 快 递 ” 的 故 事 来 形 象 理 解 :

3.1 委 托 取 快 递 故 事 版

  1. ** 委 托 申 请 ** : 你 ( 用 户 ) 在 小 明 网 站 点 击 “ 微 信 登 录 ” , 网 站 生 成 一 个 包 含 客 户 端 ID 、 回 调 地 址 、 随 机 State 参 数 的 授 权 申 请 单 , 去 找 丰 巢 授 权 中 心 ( 微 信 授 权 服 务 器 ) 。

  2. ** 授 权 确 认 ** : 丰 巢 给 你 手 机 推 送 消 息 : “ 小 明 网 站 想 代 您 取 快 递 , 允 许 吗 ? ” 你 点 击 “ 同 意 ” 。

  3. ** 获 取 临 时 取 件 码 ** : 丰 巢 给 小 明 网 站 一 个 短 期 有 效 的 临 时 取 件 码 ( 授 权 码 Code ) , 通 过 回 调 地 址 转 交 给 网 站 。

  4. ** 换 取 门 禁 卡 ** : 小 明 网 站 用 临 时 取 件 码 和 自 己 的 身 份 秘 密 ( Client Secret ) 换 取 一 张 短 期 有 效 的 门 禁 卡 ( Access Token ) 和 一 张 长 期 续 期 券 ( Refresh Token ) 。

  5. ** 取 快 递 ** : 网 站 用 门 禁 卡 打 开 丰 巢 柜 ( 微 信 资 源 服 务 器 ) , 取 出 你 的 快 递 ( 用 户 信 息 ) 。

  6. ** 完 成 登 录 ** : 网 站 根 据 快 递 信 息 创 建 或 查 找 本 地 用 户 , 生 成 网 站 通 行 证 ( JWT Token ) 返 回 给 你 。

  7. ** 续 期 逻 辑 ** : 当 门 禁 卡 过 期 时 , 网 站 用 续 期 券 ( Refresh Token ) 免 费 换 新 卡 , 无 需 你 重 新 授 权 。

3.2 技 术 流 程 图

sequenceDiagram participant 用 户 participant 网 站 participant 微 信 授 权 服 务 器 participant 微 信 资 源 服 务 器 用 户 ->> 网 站 : 点 击 微 信 登 录 网 站 ->> 微 信 授 权 服 务 器 : 重 定 向 授 权 ( 携 带 state ) 微 信 授 权 服 务 器 ->> 用 户 : 展 示 授 权 页 用 户 ->> 微 信 授 权 服 务 器 : 确 认 授 权 微 信 授 权 服 务 器 ->> 网 站 : 返 回 code + state 网 站 ->> 微 信 授 权 服 务 器 : 用 code 换 access_token + refresh_token 微 信 授 权 服 务 器 ->> 网 站 : 返 回 access_token + refresh_token + openid 网 站 ->> 微 信 资 源 服 务 器 : 用 access_token 获 取 用 户 信 息 微 信 资 源 服 务 器 ->> 网 站 : 返 回 用 户 信 息 网 站 ->> 网 站 : 存 储 refresh_token , 生 成 JWT Token 网 站 ->> 用 户 : 返 回 JWT Token Note over 用 户 , 网 站 : 后 续 请 求 中 ... 用 户 ->> 网 站 : 访 问 需 认 证 接 口 网 站 ->> 网 站 : 校 验 JWT Token 发 现 过 期 网 站 ->> 网 站 : 调 用 /refresh-token 接 口 网 站 ->> 微 信 授 权 服 务 器 : 用 refresh_token 刷 新 access_token 微 信 授 权 服 务 器 ->> 网 站 : 返 回 新 access_token + refresh_token 网 站 ->> 网 站 : 生 成 新 JWT Token 返 回 用 户

四 、 OAuth2 实 现 原 理

4.1 核 心 角 色

  • ** 资 源 所 有 者 ** : 用 户 本 人 , 拥 有 资 源 的 所 有 权 。
  • ** 客 户 端 ** : 小 明 的 网 站 , 想 获 取 用 户 资 源 。
  • ** 授 权 服 务 器 ** : 微 信 服 务 器 , 负 责 验 证 用 户 身 份 并 发 放 令 牌 。
  • ** 资 源 服 务 器 ** : 微 信 服 务 器 , 存 储 用 户 资 源 ( 如 个 人 信 息 ) 。

4.2 核 心 要 素

  • ** 授 权 码 ( Code ) ** : 短 期 有 效 的 临 时 凭 证 , 用 于 交 换 Access Token , 防 止 Token 泄 露 。
  • ** 访 问 令 牌 ( Access Token ) ** : 短 期 有 效 的 凭 证 , 用 于 访 问 资 源 服 务 器 。
  • ** 刷 新 令 牌 ( Refresh Token ) ** : 长 期 有 效 的 凭 证 , 用 于 刷 新 Access Token 。
  • ** 作 用 域 ( Scope ) ** : 授 权 的 权 限 范 围 , 如 微 信 登 录 的 snsapi_login 。

五 、 OAuth2 与 Spring Security 对 比

| ** 维 度 ** | ** OAuth2 ** | ** Spring Security ** |
| ** 定 位 ** | 授 权 框 架 , 解 决 第 三 方 授 权 问 题 | 综 合 安 全 框 架 , 解 决 认 证 + 授 权 全 流 程 |
| ** 核 心 目 标 ** | 安 全 授 权 ( 如 微 信 登 录 ) | 系 统 安 全 ( 登 录 、 权 限 校 验 、 CSRF 防 护 ) |
| ** 核 心 角 色 ** | 资 源 所 有 者 、 客 户 端 、 授 权 服 务 器 、 资 源 服 务 器 | 无 固 定 角 色 , 通 过 过 滤 器 链 实 现 安 全 控 制 |
| ** 典 型 场 景 ** | 第 三 方 登 录 、 API 接 口 授 权 | 系 统 登 录 、 接 口 权 限 校 验 、 会 话 管 理 |
| ** 关 系 ** | Spring Security 可 通 过 spring-security-oauth2 集 成 OAuth2 | 可 将 OAuth2 作 为 认 证 方 式 之 一 ( 如 社 交 登 录 ) |

六 、 完 整 保 姆 级 案 例 : 小 明 网 站 微 信 登 录

6.1 环 境 准 备

  • ** 开 发 者 账 号 ** : 在 微 信 开 放 平 台 注 册 账 号 , 创 建 网 站 应 用 , 获 取 AppID 和 AppSecret 。
  • ** 项 目 依 赖 ** : 使 用 Spring Boot 2.7.x , 引 入 相 关 依 赖 。

6.2 POM 导 入 ( 核 心 依 赖 )

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.15</version> <!-- 稳定版 --><relativePath/></parent><groupId>com.example</groupId><artifactId>wechat-login-demo</artifactId><version>1.0-SNAPSHOT</version><properties><java.version>1.8</java.version><mybatis-plus.version>3.5.3.1</mybatis-plus.version><hutool.version>5.8.20</hutool.version></properties><dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Security(用于JWT认证) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- MyBatis-Plus(数据库操作) --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- MySQL驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- JWT工具 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><!-- HTTP客户端 --><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.14</version></dependency><!-- 工具类 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

6.3 数 据 库 设 计 ( SQL 表 创 建 )

-- 用 户 表 ( 含 Refresh Token  存 储 )
CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用 户 ID',`username` varchar(50) NOT NULL COMMENT '用 户 名 ( 唯 一 )',`password` varchar(100) DEFAULT '' COMMENT '密 码 ( 本 地 登 录 用 ,  第 三 方 登 录 为 空 )',`real_name` varchar(50) DEFAULT '' COMMENT '真 实 姓 名',`avatar` varchar(255) DEFAULT '' COMMENT '头 像 URL',`provider` varchar(20) DEFAULT NULL COMMENT '第 三 方 登 录 提 供 商 ( wechat/qq/local )',`provider_id` varchar(100) DEFAULT NULL COMMENT '第 三 方 用 户 唯 一 标 识 ( openid )',`status` tinyint NOT NULL DEFAULT '1' COMMENT '状 态 ( 0 禁 用 ,  1 启 用 )',`last_login_time` datetime DEFAULT NULL COMMENT '最 后 登 录 时 间',`refresh_token` varchar(255) DEFAULT NULL COMMENT '微 信 刷 新 令 牌 ( AES  加 密 存 储 )',`refresh_token_expire_time` datetime DEFAULT NULL COMMENT '刷 新 令 牌 过 期 时 间',`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创 建 时 间',`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更 新 时 间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`),UNIQUE KEY `uk_provider_openid` (`provider`,`provider_id`) COMMENT '第 三 方 账 号 唯 一 索 引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用 户 表';-- 创 建 索 引
CREATE INDEX idx_refresh_token_expire ON sys_user(refresh_token_expire_time);

6.4 配 置 文 件 ( application.yml )

server:port: 8080servlet:context-path: /apispring:datasource:url: jdbc:mysql://localhost:3306/wechat_login_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: root123driver-class-name: com.mysql.cj.jdbc.Driver# 微 信 登 录 配 置
wechat:oauth:client-id: ${WECHAT_APP_ID:wx_your_app_id}  # 微 信 开 放 平 台 AppIDclient-secret: ${WECHAT_APP_SECRET:your_app_secret}  # 微 信 开 放 平 台 AppSecretredirect-uri: ${WECHAT_REDIRECT_URI:https://yourdomain.com/api/auth/wechat/callback}  # 授 权 回 调 地 址auth-uri: https://open.weixin.qq.com/connect/qrconnect  # 授 权 URLtoken-uri: https://api.weixin.qq.com/sns/oauth2/access_token  # 获 取 token URLuser-info-uri: https://api.weixin.qq.com/sns/userinfo  # 获 取 用 户 信 息 URLscope: snsapi_login  # 授 权 范 围token-expiration: 7200  # access_token 有 效 期 ( 秒 )# JWT 配 置
jwt:secret: ${JWT_SECRET:your_strong_secret_key_32_chars_min}  # 密 钥 ( 生 产 环 境 用 复 杂 随 机 字 符 串 )expiration: 86400000  # Token 有 效 期 ( 毫 秒 ,  24 小 时 )issuer: xiaoming-website  # 签 发 者# 加 密 配 置 ( 用 于 Refresh Token  加 密 )
crypto:aes:key: ${AES_SECRET_KEY:your_aes_secret_key_16_bytes}  # AES 密 钥 ( 16 字 节 )

6.5 核 心 代 码 实 现

6.5.1 配 置 属 性 类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;/*** 微 信 OAuth2  配 置 属 性*/
@Data
@Component
@ConfigurationProperties(prefix = "wechat.oauth")
public class WechatOAuthProperties {private String clientId;       // AppIDprivate String clientSecret;   // AppSecretprivate String redirectUri;    // 回 调 地 址private String authUri;        // 授 权 URLprivate String tokenUri;       // 获 取 token URLprivate String userInfoUri;    // 获 取 用 户 信 息 URLprivate String scope;          // 授 权 范 围private int tokenExpiration;   // token 有 效 期 ( 秒 )
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;/*** 加 密 配 置 属 性*/
@Data
@Component
@ConfigurationProperties(prefix = "crypto.aes")
public class CryptoProperties {private String key;  // AES 密 钥
}

6.5.2 JWT 工 具 类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;/*** JWT  工 具 类 ( 生 产 级 实 现 )*/
@Component
public class JwtUtils {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private long expiration; // 毫 秒@Value("${jwt.issuer}")private String issuer;// 生 成 签 名 密 钥 ( HS512  需 至 少  512  位 密 钥 )private SecretKey getSigningKey() {return Keys.hmacShaKeyFor(secret.getBytes());}/*** 生 成 JWT  令 牌*/public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put("username", userDetails.getUsername());return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername()).setIssuer(issuer).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(getSigningKey(), SignatureAlgorithm.HS512) // 生 产 环 境 用 HS512  更 安 全.compact();}/*** 从 令 牌 中 提 取 用 户 名*/public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}/*** 提 取 过 期 时 间*/public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}/*** 提 取 声 明*/private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}/*** 解 析 所 有 声 明*/private Claims extractAllClaims(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}/*** 验 证 令 牌 是 否 有 效*/public boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}/*** 检 查 令 牌 是 否 过 期*/private boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}
}

6.5.3 加 密 工 具 类

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;/*** AES  加 密 工 具 类 ( 用 于 Refresh Token  加 密 存 储 )*/
@Component
public class CryptoUtils {private final AES aes;// 构 造 函 数 注 入 密 钥public CryptoUtils(@Value("${crypto.aes.key}") String aesKey) {// 确 保 密 钥 长 度 为  16  字 节 ( AES-128  )if (aesKey.length() < 16) {aesKey = String.format("%-16s", aesKey).substring(0, 16);} else if (aesKey.length() > 16) {aesKey = aesKey.substring(0, 16);}this.aes = SecureUtil.aes(aesKey.getBytes());}/*** 加 密 字 符 串*/public String encrypt(String data) {return aes.encryptHex(data);}/*** 解 密 字 符 串*/public String decrypt(String encryptedData) {return aes.decryptStr(encryptedData);}
}

6.5.4 微 信 授 权 服 务

import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;/*** 微 信 授 权 服 务 ( 含 续 期 逻 辑 )*/
@Service
@RequiredArgsConstructor
public class WechatAuthService {private final WechatOAuthProperties wechatProps;/*** 构 建 微 信 授 权 URL ( 引 导 用 户 跳 转 )*/public String buildAuthUrl(String state) {return UriComponentsBuilder.fromHttpUrl(wechatProps.getAuthUri()).queryParam("appid", wechatProps.getClientId()).queryParam("redirect_uri", wechatProps.getRedirectUri()).queryParam("response_type", "code").queryParam("scope", wechatProps.getScope()).queryParam("state", state) // 防 CSRF  攻 击.fragment("wechat_redirect") // 微 信 要 求 的 锚 点.build().toUriString();}/*** 用 授 权 码 换 取 Token ( 含 Access Token  和 Refresh Token  )*/public TokenDTO getAccessToken(String code) {String url = UriComponentsBuilder.fromHttpUrl(wechatProps.getTokenUri()).queryParam("appid", wechatProps.getClientId()).queryParam("secret", wechatProps.getClientSecret()).queryParam("code", code).queryParam("grant_type", "authorization_code").build().toUriString();String response = HttpUtil.get(url);JSONObject json = JSONUtil.parseObj(response);// 校 验 微 信 返 回 错 误 ( 如 code  无 效 )if (json.containsKey("errcode")) {throw new RuntimeException("微 信 授 权 失 败 : " + json.getStr("errmsg"));}// 封 装 Token  信 息TokenDTO tokenDTO = new TokenDTO();tokenDTO.setAccessToken(json.getStr("access_token"));tokenDTO.setRefreshToken(json.getStr("refresh_token")); // 获 取 刷 新 令 牌tokenDTO.setOpenid(json.getStr("openid"));tokenDTO.setExpiresIn(json.getInt("expires_in", 7200)); // 有 效 期 ( 秒 )tokenDTO.setScope(json.getStr("scope"));return tokenDTO;}/*** 用 Refresh Token  刷 新 Access Token*/public TokenDTO refreshAccessToken(String refreshToken) {String url = UriComponentsBuilder.fromHttpUrl(wechatProps.getTokenUri()).queryParam("appid", wechatProps.getClientId()).queryParam("grant_type", "refresh_token").queryParam("refresh_token", refreshToken).build().toUriString();String response = HttpUtil.get(url);JSONObject json = JSONUtil.parseObj(response);if (json.containsKey("errcode")) {throw new RuntimeException("刷 新 Token  失 败 : " + json.getStr("errmsg"));}TokenDTO tokenDTO = new TokenDTO();tokenDTO.setAccessToken(json.getStr("access_token"));tokenDTO.setRefreshToken(json.getStr("refresh_token")); // 微 信 可 能 返 回 新 的 refresh_tokentokenDTO.setOpenid(json.getStr("openid"));tokenDTO.setExpiresIn(json.getInt("expires_in", 7200));return tokenDTO;}/*** 用 Access Token  获 取 用 户 信 息*/public SocialUserInfo getUserInfo(String accessToken, String openid) {String url = UriComponentsBuilder.fromHttpUrl(wechatProps.getUserInfoUri()).queryParam("access_token", accessToken).queryParam("openid", openid).queryParam("lang", "zh_CN").build().toUriString();String response = HttpUtil.get(url);JSONObject json = JSONUtil.parseObj(response);if (json.getInt("errcode", 0) != 0) {throw new RuntimeException("微 信 API  错 误 : " + json.getStr("errmsg"));}SocialUserInfo info = new SocialUserInfo();info.setOpenid(json.getStr("openid"));info.setNickname(json.getStr("nickname"));info.setAvatar(json.getStr("headimgurl"));info.setGender(json.getInt("sex", 0) == 1 ? "男" : (json.getInt("sex", 0) == 2 ? "女" : "未知"));info.setProvider("wechat");return info;}/*** Token  信 息 封 装 类*/@lombok.Datapublic static class TokenDTO {private String accessToken;private String refreshToken;private String openid;private int expiresIn; // 有 效 期 ( 秒 )private String scope;}
}

6.5.5 用 户 服 务

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;/*** 用 户 服 务 ( 含 Refresh Token  存 储 与 刷 新 )*/
@Service
@RequiredArgsConstructor
public class UserService extends ServiceImpl<SysUserMapper, SysUser> {private final WechatAuthService wechatAuthService;private final CryptoUtils cryptoUtils;private final PasswordEncoder passwordEncoder;/*** 根 据 第 三 方 信 息 查 找 / 创 建 用 户 ( 含 Token  存 储 )*/@Transactional(rollbackFor = Exception.class)public SysUser findOrCreateBySocialInfo(SocialUserInfo socialInfo, WechatAuthService.TokenDTO tokenDTO) {// 1. 查 找 是 否 已 绑 定 该 第 三 方 账 号SysUser user = lambdaQuery().eq(SysUser::getProvider, socialInfo.getProvider()).eq(SysUser::getProviderId, socialInfo.getProviderId()).one();// 2. 若 用 户 已 存 在  ,  更 新 Token  信 息 和 登 录 时 间if (user != null) {user.setRefreshToken(cryptoUtils.encrypt(tokenDTO.getRefreshToken())); // AES  加 密 存 储user.setRefreshTokenExpireTime(calculateExpireTime(tokenDTO.getExpiresIn()));user.setLastLoginTime(LocalDateTime.now());updateById(user);return user;}// 3. 新 用 户 : 生 成 账 号 并 存 储 Tokenuser = new SysUser();user.setUsername(generateUniqueUsername(socialInfo.getNickname()));user.setPassword(""); // 第 三 方 登 录 无 密 码user.setRealName(socialInfo.getNickname());user.setAvatar(socialInfo.getAvatar());user.setProvider(socialInfo.getProvider());user.setProviderId(socialInfo.getProviderId());user.setStatus(1); // 启 用 状 态user.setRefreshToken(cryptoUtils.encrypt(tokenDTO.getRefreshToken())); // AES  加 密 存 储user.setRefreshTokenExpireTime(calculateExpireTime(tokenDTO.getExpiresIn()));user.setCreatedAt(LocalDateTime.now());user.setUpdatedAt(LocalDateTime.now());save(user);return user;}/*** 用 Refresh Token  刷 新 用 户 的 Access Token*/@Transactional(rollbackFor = Exception.class)public SysUser refreshUserToken(Long userId) {SysUser user = getById(userId);if (user == null || StrUtil.isBlank(user.getRefreshToken())) {throw new RuntimeException("用 户 未 绑 定 微 信 或 Refresh Token  已 失 效 ");}// 1. 解 密 Refresh TokenString refreshToken = cryptoUtils.decrypt(user.getRefreshToken());// 2. 调 用 微 信 接 口 刷 新 TokenWechatAuthService.TokenDTO newToken = wechatAuthService.refreshAccessToken(refreshToken);// 3. 更 新 用 户 的 Token  信 息user.setRefreshToken(cryptoUtils.encrypt(newToken.getRefreshToken()));user.setRefreshTokenExpireTime(calculateExpireTime(newToken.getExpiresIn()));user.setUpdatedAt(LocalDateTime.now());updateById(user);return user;}/*** 生 成 唯 一 用 户 名 ( 避 免 重 复 )*/private String generateUniqueUsername(String nickname) {String baseName = nickname.replaceAll("[^a-zA-Z0-9_]", "");if (baseName.length() < 3) {baseName = "user_" + System.currentTimeMillis();}String username = baseName;int suffix = 1;while (lambdaQuery().eq(SysUser::getUsername, username).count() > 0) {username = baseName + "_" + suffix++;}return username;}/*** 计 算 Token  过 期 时 间*/private LocalDateTime calculateExpireTime(int expiresInSeconds) {return LocalDateTime.now().plusSeconds(expiresInSeconds);}
}

6.5.6 控 制 器

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collections;
import java.util.UUID;/*** 认 证 控 制 器 ( 含 续 期 接 口 )*/
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {private final WechatAuthService wechatAuthService;private final UserService userService;private final JwtUtils jwtUtils;/*** 微 信 登 录 入 口 ( 重 定 向 到 微 信 授 权 页 )*/@GetMapping("/wechat/login")public void wechatLogin(HttpServletResponse response, HttpSession session) throws IOException {// 生 成 随 机 state  参 数 ( 防 CSRF  攻 击 )String state = UUID.randomUUID().toString();session.setAttribute("wechat_oauth_state", state); // 存 储 到 Session// 构 建 授 权 URL  并 重 定 向String authUrl = wechatAuthService.buildAuthUrl(state);response.sendRedirect(authUrl);}/*** 微 信 授 权 回 调 ( 微 信 重 定 向 到 这 里 )*/@GetMapping("/wechat/callback")public ModelAndView wechatCallback(@RequestParam String code,@RequestParam String state,HttpSession session,HttpServletRequest request) {// 1. 验 证 state  参 数 ( 防 CSRF  攻 击 )String savedState = (String) session.getAttribute("wechat_oauth_state");if (savedState == null || !savedState.equals(state)) {return new ModelAndView("redirect:/login?error=invalid_state");}try {// 2. 用 code  换 取 Token ( 含 access_token  和 refresh_token  )WechatAuthService.TokenDTO tokenDTO = wechatAuthService.getAccessToken(code);String accessToken = tokenDTO.getAccessToken();String openid = tokenDTO.getOpenid();// 3. 获 取 用 户 信 息SocialUserInfo userInfo = wechatAuthService.getUserInfo(accessToken, openid);// 4. 查 找 / 创 建 本 地 用 户 ( 存 储 refresh_token  )SysUser sysUser = userService.findOrCreateBySocialInfo(userInfo, tokenDTO);// 5. 生 成 JWT  令 牌UserDetails userDetails = new User(sysUser.getUsername(),sysUser.getPassword(),Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));String token = jwtUtils.generateToken(userDetails);// 6. 重 定 向 到 前 端 ( 携 带 token  )return new ModelAndView("redirect:https://yourfrontend.com/login/success?token=" + token);} catch (Exception e) {// 记 录 错 误 日 志 ( 生 产 环 境 用 日 志 框 架 )e.printStackTrace();return new ModelAndView("redirect:/login?error=" + e.getMessage());} finally {// 清 除 Session  中 的 statesession.removeAttribute("wechat_oauth_state");}}/*** 刷 新 Token  接 口 ( 前 端 定 时 调 用 )*/@PostMapping("/refresh-token")public Result<String> refreshToken(@RequestHeader("Authorization") String authHeader) {// 1. 从 Header  提 取 旧 JWT TokenString oldToken = authHeader.replace("Bearer ", "");String username = jwtUtils.extractUsername(oldToken);// 2. 查 询 用 户SysUser user = userService.lambdaQuery().eq(SysUser::getUsername, username).one();if (user == null) {return Result.error("用 户 不 存 在 ");}// 3. 刷 新 用 户 TokenSysUser updatedUser = userService.refreshUserToken(user.getId());// 4. 用 新 的 Refresh Token  获 取 用 户 信 息 ( 验 证 有 效 性 )String refreshToken = cryptoUtils.decrypt(updatedUser.getRefreshToken());WechatAuthService.TokenDTO newToken = wechatAuthService.refreshAccessToken(refreshToken);SocialUserInfo userInfo = wechatAuthService.getUserInfo(newToken.getAccessToken(), newToken.getOpenid());// 5. 生 成 新 的 JWT Token  返 回UserDetails userDetails = new User(updatedUser.getUsername(),"",Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));String newJwtToken = jwtUtils.generateToken(userDetails);return Result.success(newJwtToken);}// 简 化 版 Result  响 应 类static class Result<T> {private int code;private String msg;private T data;public static <T> Result<T> success(T data) {Result<T> result = new Result<>();result.code = 200;result.msg = "成 功 ";result.data = data;return result;}public static <T> Result<T> error(String msg) {Result<T> result = new Result<>();result.code = 500;result.msg = msg;return result;}}
}

6.5.7 Spring Security 配 置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity
public class SecurityConfig {// 密 码 编 码 器 ( 生 产 环 境 用 BCrypt  )@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 安 全 过 滤 链 配 置@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()) // 前 后 端 分 离 禁 用 CSRF.authorizeHttpRequests(auth -> auth.requestMatchers("/auth/**").permitAll() // 登 录 相 关 接 口 放 行.anyRequest().authenticated() // 其 他 接 口 需 认 证).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无 状 态 ( JWT  ));return http.build();}
}

6.6 前 端 集 成 示 例

<!DOCTYPE html>
<html>
<head><title>微 信 登 录 示 例</title>
</head>
<body><h1>第 三 方 登 录 演 示</h1><button onclick="loginWithWechat()">微 信 登 录</button><script>// 跳 转 到 微 信 登 录function loginWithWechat() {window.location.href = "/api/auth/wechat/login";}// 处 理 登 录 成 功 后 的 Tokenfunction handleLoginSuccess(token) {localStorage.setItem("jwt_token", token);alert("登 录 成 功 !");// 跳 转 到 首 页window.location.href = "/home";}// 解 析 URL  参 数 ( 接 收 后 端 重 定 向 的 token  )function getUrlParam(name) {const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");const r = window.location.search.substr(1).match(reg);return r ? decodeURIComponent(r[2]) : null;}// 页 面 加 载 时 检 查 是 否 有 token  参 数window.onload = function() {const token = getUrlParam("token");if (token) {handleLoginSuccess(token);}};// 定 时 续 期 逻 辑let refreshTimer;function startAutoRefresh(token) {const payload = JSON.parse(atob(token.split('.')[1]));const expTime = payload.exp * 1000;const refreshTime = expTime - 30 * 60 * 1000; // 提 前  30  分 钟 续 期refreshTimer = setTimeout(async () => {try {const response = await fetch('/api/auth/refresh-token', {method: 'POST',headers: {'Authorization': `Bearer ${token}`}});const result = await response.json();if (result.code === 200) {const newToken = result.data;localStorage.setItem('jwt_token', newToken);startAutoRefresh(newToken);} else {throw new Error(result.msg);}} catch (e) {console.error('续 期 失 败 ,  需 重 新 登 录', e);window.location.href = '/login';}}, refreshTime - Date.now());}// 登 录 成 功 后 启 动 续 期const token = localStorage.getItem('jwt_token');if (token) {startAutoRefresh(token);}</script>
</body>
</html>

七 、 生 产 环 境 优 化 建 议

  1. ** 安 全 增 强 **

    • 使 用 HTTPS 强 制 加 密 传 输
    • 在 Redis 中 存 储 State 参 数 ( 分 布 式 系 统 )
    • JWT 密 钥 定 期 轮 换 ( 每 90 天 )
    • 使 用 RS256 非 对 称 加 密 替 代 HS256
  2. ** 健 壮 性 优 化 **

    • 添 加 接 口 限 流 ( Redis 计 数 )
    • 记 录 关 键 日 志 ( 授 权 流 程 、 Token 刷 新 )
    • 实 现 错 误 重 试 机 制 ( 最 多 3 次 )
  3. ** 可 扩 展 性 **

    • 抽 象 OAuth2 服 务 接 口 , 支 持 多 个 第 三 方 登 录
    • 提 供 用 户 绑 定 账 号 功 能 ( 输 入 用 户 名 密 码 关 联 )
    • 集 成 Redis 存 储 在 线 用 户 状 态

八 、 部 署 与 测 试

  1. ** 部 署 步 骤 **

    • 准 备 服 务 器 ( CentOS/Ubuntu ), 安 装 JDK 1.8+ 、 MySQL 8.0+
    • 配 置 环 境 变 量 ( 注 入 WECHAT_APP_ID 、 WECHAT_APP_SECRET 、 JWT_SECRET 等 )
    • 打 包 项 目 : mvn clean package -DskipTests
    • 启 动 应 用 : java -jar target/wechat-login-demo-1.0-SNAPSHOT.jar
  2. ** 测 试 流 程 **

    • 访 问 登 录 页 , 点 击 “ 微 信 登 录 ”
    • 跳 转 微 信 授 权 页 , 确 认 授 权
    • 微 信 重 定 向 回 回 调 地 址 , 后 端 生 成 JWT 并 返 回 前 端
    • 前 端 存 储 JWT , 后 续 请 求 携 带 Authorization: Bearer {token} 头

通 过 以 上 步 骤 , 小 明 成 功 为 网 站 集 成 了 微 信 登 录 功 能 , 用 户 体 验 大 幅 提 升 , 网 站 注 册 转 化 率 提 高 了 30% 。 这 也 让 小 明 深 刻 认 识 到 , OAuth2 作 为 一 种 安 全 的 授 权 标 准 , 在 第 三 方 登 录 场 景 中 发 挥 着 不 可 或 缺 的 作 用 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/976829.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年11月机器人油脂公司推荐榜:五家优质供应商综合对比与权威评测

在工业自动化浪潮中,机器人油脂的选择成为众多工程师与采购决策者的关键课题。随着工业机器人应用场景的不断拓展,从汽车制造到电子装配,从物流仓储到精密加工,对润滑油脂的性能要求日益严苛。根据行业调研数据显示…

2025年11月国标隔热条厂家综合选择指南:专业推荐与采购建议

摘要 随着建筑节能标准的不断提高,国标隔热条行业在2025年迎来了快速发展期。本文基于市场调研和用户反馈,整理了当前市场上表现优异的国标隔热条生产厂家排名,为有采购需求的用户提供参考。同时附上各家企业的详细…

2025年11月网络推广公司推荐榜单:五大服务商综合对比分析

随着数字化转型浪潮席卷各行各业,越来越多的企业意识到网络推广的重要性。无论是传统制造业寻求线上突破,还是新兴品牌希望快速打开市场,选择一家专业可靠的网络推广公司都成为关键决策。当前市场上网络推广服务商数…

2025年导视设计公司综合推荐排行榜

摘要 随着商业空间体验升级需求激增,导视设计行业在2025年迎来快速发展期。本文基于市场调研数据和用户反馈,整理出当前市场上表现突出的五家导视设计服务商,为有需求的企业提供参考选择。文末附有详细的挑选指南,…

2025年11月MPP电力管品牌综合实力排行榜TOP10

摘要 随着城市电网改造和新能源建设的快速发展,MPP电力管作为电力电缆保护的重要材料,在2025年迎来了新一轮需求高峰。本文基于市场调研、技术参数对比和用户反馈,为您呈现当前MPP电力管领域的权威排名,并提供详细…

As of 2025|中国云计算三强格局确立:AWS、华为云、阿里云并立

2025年,中国云计算市场正式进入“三强时代”。 根据 IDC China Cloud Market Report 2025 与 中国信通院云计算白皮书, AWS(Amazon Web Services)、华为云(Huawei Cloud)、阿里云(Alibaba Cloud) 已形成稳固的…

2025年11月PE管品牌综合评测与选购指南:十大权威厂家排行揭晓

摘要 随着城市化进程加速和基础设施建设的不断推进,PE管行业在2025年迎来了新的发展机遇。PE管以其耐腐蚀、高强度、长寿命等优异性能,在给排水、燃气输送、工业管道等领域得到广泛应用。本文基于市场调研和行业数据…

【ArcMap】将1984 的坐标系转化为大地2000

点击菜单栏上的 Geoprocessing,然后选择 ArcToolbox,或者直接点击标准工具栏上的红色工具箱图标。在打开的 ArcToolbox 窗口中,依次展开:Data Management Tools -> Projections and Transformations -> Feat…

【IEEE出版 | EI、Scopus、IEEE Xplore三检索】第四届机械电子工程与人工智能国际学术会议(MEAI 2025)

2025年第四届机械电子工程与人工智能国际学术会议(MEAI 2025)将于2025年12月5日至7日在美丽的辽宁省沈阳市隆重召开。【见刊检索速度较快、对学生稿件友好、会议现场学术氛围浓厚】 第四届机械电子工程与人工智能国际…

2025 最新工程模具厂家推荐榜:框格梁 / 混凝土 / 护坡 / 花窗模具权威测评,实力源头厂家优选指南

引言 随着全球基础设施建设的高速发展,模具作为工程核心装备,其精度与耐用性直接决定项目品质与效率。据国际模具及塑胶制品协会(ISTMA)最新数据显示,高端工程模具精度已迈入 0.005mm 级标准,而市场中 35% 的厂商…

2025 年 11 月切膜机厂家权威推荐榜:覆盖自动切膜机、激光切膜机、高速切膜机、智能切膜机、不干胶切膜机等,精准高效助力生产升级!

2025 年 11 月切膜机厂家权威推荐榜:覆盖自动切膜机、激光切膜机、高速切膜机、智能切膜机、不干胶切膜机等,精准高效助力生产升级! 随着制造业向智能化、高效化转型,切膜机作为关键设备,在提升生产效率和产品质量…

体验一款在线工具:一键轻松去除快手视频水印

在日常浏览短视频时,我们偶尔会遇到希望保存下来的精彩片段,但平台的水印却影响了分享或收藏的观感。最近,我发现了一款专为解决此问题而设计的在线工具,它主打为快手视频去除水印,操作十分便捷。下面就来简单体验…

鸿蒙NDK构建实战指南:从ArkTS到C/C++的高性能桥梁

鸿蒙NDK构建实战指南:从ArkTS到C/C++的高性能桥梁pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas"…

2025年浙江汽车零部件网站建设公司权威推荐榜单:独立站建设‌/学校网站建设‌/网页设计‌源头公司精选

在2025年浙江汽车零部件产业数字化浪潮中,高达68%的采购商通过专业企业网站评估供应商资质,一个具备国际视野、技术展示能力和用户体验至上的网站已成为企业竞争力的关键组成部分。 浙江作为中国汽车零部件产业的重要…

2025 最新模板厂家权威推荐榜:塑钢 / 水沟 / 现浇等多类型模板优质厂家深度测评水池 / 方墩 / 框格梁 / 菜地沟 / 挡土墙模板推荐

引言 在全球基建领域持续升级的背景下,模板作为工程核心施工材料,其品质直接决定工程精度与效率。国际模板与脚手架协会(IFSMA)最新测评数据显示,全球模板市场合格产品仅占 68%,超三成产品存在强度不足、周转次数…

上海办公室商铺装修哪家强?2025 双场景 TOP10 强企全攻略(附精准选型)

上海办公室&商铺装修哪家强?2025 双场景 TOP10 强企全攻略(附精准选型)在上海做办公室或商铺装修,选对服务商能少走 80% 的弯路 —— 既避免工期延误、增项加价的坑,又能让空间适配办公效率或经营需求。结合《…

2025 年 11 月刻字机厂家权威推荐榜:智能刻字机,激光刻字机,金属刻字机,石材刻字机,全自动刻字机,工业刻字机,木雕刻字机,异形刻字机,高精度高效能设备精选!

2025年11月刻字机厂家权威推荐榜:智能刻字机,激光刻字机,金属刻字机,石材刻字机,全自动刻字机,工业刻字机,木雕刻字机,异形刻字机,高精度高效能设备精选! 行业技术发展现状 刻字设备制造业正经历着智能化、精…

2025 年 11 月中国电线电缆厂家最新推荐,技术实力与市场口碑深度解析!

引言 随着全球电力基础设施升级与新能源产业快速发展,电线电缆作为核心传输载体,其品质与性能成为工程安全与效率的关键。本次 2025 年 11 月电线电缆厂家推荐,参考国际电线电缆协会(ICEA)、国际电工委员会(IEC)…

2025年质量好的史馆展厅设计用户满意度榜

2025年质量好的史馆展厅设计用户满意度榜行业背景与市场趋势随着我国文化产业的蓬勃发展和数字化转型的深入推进,史馆展厅设计行业迎来了前所未有的发展机遇。据中国展览行业协会最新数据显示,2024年中国展厅设计市场…

谁在引领中国云市场?Huawei、Alibaba、AWS 成为新主流

2025年当下,中国云计算市场已确立了以 Huawei Cloud、Alibaba Cloud 与 Amazon Web Services(AWS)为核心的三强阵营。IDC与Gartner数据表明,三者合计拿下超60%的市场份额,构成中国“Hybrid Cloud + AI-native”生…