核心思路是:
- 小程序端: 调用微信提供的 API 获取用户的加密手机号数据。
- 小程序端: 将加密数据发送给后端。
- 后端:
- 接收小程序发送的加密数据。
- 使用微信提供的 SDK 解密手机号。
- 根据解密后的手机号在
sys_user表中查找用户。 - 如果用户存在,则为该用户生成一个 JWT token,并返回给小程序。
- 如果用户不存在,可以选择:
- 拒绝登录(用户未注册)。
- 自动注册新用户(推荐,提供更好的用户体验)。
- 将生成的 JWT token 返回给前端,前端后续请求带上这个 token 进行认证。
下面我将分步骤为您实现。
第一部分:后端 Spring Boot 实现
1. 准备工作:微信开放平台配置
您需要在微信开放平台(mp.weixin.qq.com 或 open.weixin.qq.com)配置您的小程序:
- 获取
AppID和AppSecret。 - 确认您的小程序已开通“获取用户手机号”的能力。
2. Maven 依赖
在 pom.xml 中添加 weixin-java-miniapp SDK(或类似提供微信 API 接口的库)以及 HTTP 客户端(如 Apache HttpClient 或 OkHttp,如果 SDK 内部没有集成)。
<?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">
<!-- ... 省略其他配置 ... -->
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Dependencies -->
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 数据库相关依赖 (假设您使用的是 MySQL + MyBatis/JPA) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.33</version> <!-- 或更高版本 -->
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version> <!-- 或更高版本 -->
</dependency>
<!-- 或 spring-boot-starter-data-jpa -->
<!-- 微信小程序 Java SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.6.0</version> <!-- 请使用最新稳定版本 -->
</dependency>
<!-- Jasypt (可选,用于加密配置文件中的敏感信息,如AppSecret) -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!-- Other dependencies... -->
</dependencies>
<!-- ... 省略其他配置 ... -->
</project>
3. 配置微信小程序参数
在 application.yml 或 application.properties 中添加微信小程序的 AppID 和 AppSecret。强烈建议使用 Jasypt 等工具对敏感信息进行加密。
application.yml 示例:
# Jasypt 加密配置 (可选,但推荐)
jasypt:
encryptor:
password: your_jasypt_secret_key # 用于加密和解密的密钥,请替换成强密码
# 微信小程序配置
wechat:
miniapp:
configs:
- app-id: your_wechat_miniapp_appid # 请替换为你的小程序AppID
# 使用 Jasypt 加密 AppSecret
# app-secret: ENC(your_encrypted_app_secret_here)
app-secret: your_wechat_miniapp_appsecret # 未加密示例,生产环境请加密
token:
aes-key:
msg-data-format: JSON
4. 微信小程序配置类
创建一个配置类来初始化 WxMaService。
src/main/java/com/example/yourapp/config/WxMaConfiguration.java
package com.example.yourapp.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.WxMaConfig;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
@ConfigurationProperties(prefix = "wechat.miniapp")
@Data
public class WxMaConfiguration {
private List<Map<String, String>> configs;
@Bean
public WxMaService wxMaService() {
if (configs == null || configs.isEmpty()) {
throw new RuntimeException("请在 application.yml 中配置 wechat.miniapp.configs 属性");
}
WxMaService service = new WxMaServiceImpl();
service.setMultiConfigs(configs.stream()
.map(a -> {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(a.get("app-id"));
config.setSecret(a.get("app-secret"));
if (a.get("token") != null) config.setToken(a.get("token"));
if (a.get("aes-key") != null) config.setAesKey(a.get("aes-key"));
if (a.get("msg-data-format") != null) config.setMsgDataFormat(a.get("msg-data-format"));
return config;
})
.collect(Collectors.toMap(WxMaConfig::getAppid, a -> a)));
return service;
}
}
5. 现有 JWT 相关类 (假设已存在)
JwtTokenUtil/JwtService: 用于生成和验证 JWT token。UserDetailsServiceImpl: 实现UserDetailsService,从数据库加载用户信息。JwtAuthenticationFilter: 解析请求头中的 JWT token 并设置 Spring Security 上下文。SecurityConfig: 配置 Spring Security 链,允许特定路径(如登录接口)无需认证,并添加 JWT 过滤器。
这里只给出 JwtTokenUtil 的示例,其他 Spring Security 和 JWT 相关的类,请根据您现有项目结构进行调整。
src/main/java/com/example/yourapp/security/JwtTokenUtil.java
package com.example.yourapp.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
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 java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret; // 从配置文件读取,请确保足够复杂
@Value("${jwt.expiration}")
private long expiration; // token 有效期,单位毫秒
// 从 token 中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从 token 中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 解析 JWT 的 Claims
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();
}
// 检查 token 是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成 token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 可以在 claims 中添加更多用户信息,例如 userId, role 等
return doGenerateToken(claims, userDetails.getUsername());
}
// 生成 token 的具体实现
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 验证 token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 获取签名密钥
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(this.secret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
在 application.yml 中添加 JWT 配置:
jwt:
# JWT 签名密钥,请替换为一个足够长和复杂的随机字符串,生产环境务必加密或从环境变量获取
secret: your_jwt_secret_key_that_is_at_least_256_bit_long_for_HS256_algorithm
expiration: 3600000 # token 有效期,1小时 (毫秒)
6. 用户实体和 Mapper (假设 sys_user 表)
假设您有一个 SysUser 实体,其中包含 phone 字段。
src/main/java/com/example/yourapp/model/SysUser.java
package com.example.yourapp.model;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections; // 假设默认没有权限
@Data
public class SysUser implements UserDetails {
private Long id;
private String username;
private String password; // 存储加密后的密码
private String phone; // 手机号
private Boolean enabled; // 用户是否可用
// 实现 UserDetails 接口方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 返回用户权限列表,此处简化为无权限
return Collections.emptyList();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled != null ? this.enabled : true;
}
}
src/main/java/com/example/yourapp/mapper/SysUserMapper.java (使用 MyBatis 示例)
package com.example.yourapp.mapper;
import com.example.yourapp.model.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
@Mapper
public interface SysUserMapper {
@Select("SELECT id, username, password, phone, enabled FROM sys_user WHERE username = #{username}")
SysUser findByUsername(@Param("username") String username);
@Select("SELECT id, username, password, phone, enabled FROM sys_user WHERE phone = #{phone}")
SysUser findByPhone(@Param("phone") String phone);
@Insert("INSERT INTO sys_user(username, password, phone, enabled) VALUES(#{username}, #{password}, #{phone}, #{enabled})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insertUser(SysUser user);
}
7. 用户服务 (UserService)
src/main/java/com/example/yourapp/service/UserService.java
package com.example.yourapp.service;
import com.example.yourapp.mapper.SysUserMapper;
import com.example.yourapp.model.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.password.PasswordEncoder; // 用于自动注册时生成虚拟密码
import java.util.UUID;
@Service
public class UserService implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private PasswordEncoder passwordEncoder; // 注入 PasswordEncoder
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
return user;
}
public SysUser findUserByPhone(String phone) {
return sysUserMapper.findByPhone(phone);
}
/**
* 注册新用户 (用于手机号登录时自动注册)
* @param phone 手机号
* @return 注册成功的用户
*/
public SysUser registerNewUser(String phone) {
SysUser newUser = new SysUser();
// 自动生成一个用户名,例如 phone 或者 random UUID
newUser.setUsername("wxuser_" + phone);
// 生成一个随机且加密的密码,因为微信登录不需要密码,但Spring Security需要
newUser.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
newUser.setPhone(phone);
newUser.setEnabled(true);
sysUserMapper.insertUser(newUser);
return newUser;
}
}
8. 微信手机号登录 DTO
src/main/java/com/example/yourapp/dto/WechatPhoneLoginRequest.java
package com.example.yourapp.dto;
import lombok.Data;
@Data
public class WechatPhoneLoginRequest {
private String code; // 小程序登录凭证
private String encryptedData; // 小程序获取手机号的加密数据
private String iv; // 加密算法的初始向量
}
src/main/java/com/example/yourapp/dto/AuthResponse.java
package com.example.yourapp.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String token;
private String message;
private String username;
private String phone;
// 可以添加更多用户信息
}
9. 认证控制器
src/main/java/com/example/yourapp/controller/AuthController.java
package com.example.yourapp.controller;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.binarywang.wx.miniapp.util.WxMaConfigHolder;
import com.example.yourapp.dto.AuthResponse;
import com.example.yourapp.dto.WechatPhoneLoginRequest;
import com.example.yourapp.model.SysUser;
import com.example.yourapp.security.JwtTokenUtil;
import com.example.yourapp.service.UserService;
import me.chanjar.weixin.common.error.WxErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@CrossOrigin(origins = "*") // 允许跨域
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private WxMaService wxMaService;
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
// 假设您的用户名/密码登录接口已存在
// @PostMapping("/login")
// public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception { ... }
/**
* 微信小程序手机号登录接口
*
* @param request 包含 code, encryptedData, iv
* @return JWT Token
*/
@PostMapping("/wx/phoneLogin")
public ResponseEntity<?> wechatPhoneLogin(@RequestBody WechatPhoneLoginRequest request) {
String appid = wxMaService.getWxMaConfig().getAppid(); // 获取当前配置的AppID
logger.info("微信手机号登录请求:AppID={}, code={}, encryptedData={}, iv={}", appid, request.getCode(), request.getEncryptedData(), request.getIv());
// 1. 获取 session_key 和 openid
WxMaJscode2SessionResult session;
try {
session = wxMaService.getUserService().getSessionInfo(request.getCode());
} catch (WxErrorException e) {
logger.error("微信jscode2session失败: {}", e.getMessage(), e);
return ResponseEntity.badRequest().body(new AuthResponse(null, "获取微信会话失败", null, null));
}
String sessionKey = session.getSessionKey();
logger.info("获取到sessionKey: {}", sessionKey);
// 2. 解密手机号
WxMaPhoneNumberInfo phoneNumberInfo;
try {
// 注意:SDK 内部会根据 appid 找到对应的 WxMaConfigImpl,从而获取到 appSecret。
// 但解密手机号操作实际需要 sessionKey 和加密数据。
// WxMaService 默认的解密方法需要提供 sessionKey。
WxMaConfigHolder.set(appid); // 确保当前线程使用正确的WxMaConfig
phoneNumberInfo = wxMaService.getUserService().getPhoneNumberInfo(sessionKey, request.getEncryptedData(), request.getIv());
} catch (Exception e) { // 捕获更广泛的异常,因为解密可能抛出 IllegalArgumentException
logger.error("解密微信手机号失败: encryptedData={}, iv={}, sessionKey={}, error={}",
request.getEncryptedData(), request.getIv(), sessionKey, e.getMessage(), e);
return ResponseEntity.badRequest().body(new AuthResponse(null, "解密手机号失败,请重试", null, null));
} finally {
WxMaConfigHolder.remove(); // 清除当前线程的WxMaConfig
}
String phoneNumber = phoneNumberInfo.getPhoneNumber();
if (phoneNumber == null || phoneNumber.isEmpty()) {
logger.warn("微信小程序手机号为空,无法登录。encryptedData={}, iv={}", request.getEncryptedData(), request.getIv());
return ResponseEntity.badRequest().body(new AuthResponse(null, "未能获取到有效的手机号", null, null));
}
logger.info("成功解密手机号: {}", phoneNumber);
// 3. 根据手机号查找或注册用户
SysUser user = userService.findUserByPhone(phoneNumber);
if (user == null) {
// 用户不存在,自动注册
user = userService.registerNewUser(phoneNumber);
logger.info("新用户注册成功,手机号: {}", phoneNumber);
}
// 4. 生成 JWT Token
// Spring Security 认证 (可选,如果只是生成token可省略)
UserDetails userDetails = userService.loadUserByUsername(user.getUsername());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); // 将认证信息放入安全上下文
String token = jwtTokenUtil.generateToken(userDetails);
logger.info("用户 {} (手机号: {}) 登录成功,生成JWT Token。", user.getUsername(), phoneNumber);
return ResponseEntity.ok(new AuthResponse(token, "登录成功", user.getUsername(), phoneNumber));
}
}
10. Security 配置类 (示例,请根据您现有配置调整)
确保 /auth/wx/phoneLogin 路径是允许未认证访问的。
src/main/java/com/example/yourapp/config/SecurityConfig.java
package com.example.yourapp.config;
import com.example.yourapp.security.JwtAuthenticationEntryPoint;
import com.example.yourapp.security.JwtRequestFilter; // 假设您的JWT过滤器类名为JwtRequestFilter
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter; // 注入您的JWT过滤器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF
.authorizeHttpRequests(authorize -> authorize
// 允许未经认证访问的接口
.requestMatchers("/auth/login", "/auth/wx/phoneLogin", "/public/**").permitAll()
// 所有其他请求都需要认证
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint) // 配置认证入口点
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
);
// 添加 JWT 过滤器在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
JwtRequestFilter.java (示例)
package com.example.yourapp.security;
import com.example.yourapp.service.UserService;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserService userService; // 使用 UserService 来加载 UserDetails
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token 在 "Bearer token" 形式
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.warn("JWT Token 不合法", e);
} catch (ExpiredJwtException e) {
logger.warn("JWT Token 已过期", e);
}
} else {
logger.warn("JWT Token 不在 Bearer 格式");
}
// 验证 Token
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userService.loadUserByUsername(username);
// 如果 token 有效,则配置 Spring Security 来手动设置认证
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 在上下文设置认证后,当前用户被认为是已认证。
// 这通过 Spring Security Conext 进行后续检查
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
JwtAuthenticationEntryPoint.java (示例)
package com.example.yourapp.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.Serializable;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 当用户尝试访问需要认证的资源,但未提供或提供了无效的认证凭据时,此方法会被调用
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
第二部分:微信小程序端实现
1. 页面 (WXML)
<!-- pages/login/login.wxml -->
<view class="container">
<button
wx:if="{{canIUseGetUserPhoneNumber}}"
open-type="getPhoneNumber"
bindgetphonenumber="getPhoneNumber"
class="wechat-phone-login-button"
>
微信一键登录
</button>
<view wx:else class="tip-text">
请升级微信版本以使用一键登录功能。
</view>
<!-- 如果您还需要传统的手机号+密码登录 -->
<view class="divider">
<text>或</text>
</view>
<input class="login-input" placeholder="请输入用户名" bindinput="handleUsernameInput" />
<input class="login-input" placeholder="请输入密码" password bindinput="handlePasswordInput" />
<button bindtap="loginByUsernamePassword" class="normal-login-button">
用户名密码登录
</button>
</view>
2. 逻辑 (JS)
pages/login/login.js
// pages/login/login.js
const app = getApp(); // 获取全局 app 实例
Page({
data: {
canIUseGetUserPhoneNumber: wx.canIUse('button.open-type.getPhoneNumber'),
username: '',
password: ''
},
onLoad: function() {
// 可以在这里检查登录状态,例如是否有缓存的 token
// wx.getStorageSync('jwt_token');
},
handleUsernameInput(e) {
this.setData({ username: e.detail.value });
},
handlePasswordInput(e) {
this.setData({ password: e.detail.value });
},
loginByUsernamePassword() {
const { username, password } = this.data;
if (!username || !password) {
wx.showToast({
title: '请输入用户名和密码',
icon: 'none'
});
return;
}
wx.request({
url: `${app.globalData.apiBaseUrl}/auth/login`, // 你的用户名密码登录接口
method: 'POST',
header: {
'content-type': 'application/json'
},
data: {
username: username,
password: password
},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.token) {
wx.setStorageSync('jwt_token', res.data.token);
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500,
success: () => {
// 登录成功后跳转到主页
setTimeout(() => {
wx.switchTab({
url: '/pages/index/index' // 你的主页路径
});
}, 1500);
}
});
} else {
wx.showToast({
title: res.data.message || '登录失败,请检查用户名或密码',
icon: 'none'
});
}
},
fail: (err) => {
console.error('用户名密码登录请求失败', err);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
}
});
},
getPhoneNumber: function (e) {
// 获取到用户授权的加密数据
console.log('getPhoneNumber event:', e);
if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
wx.showToast({
title: '用户拒绝授权获取手机号',
icon: 'none'
});
return;
}
// 1. 获取微信登录凭证 code
wx.login({
success: (loginRes) => {
if (loginRes.code) {
console.log('wx.login success, code:', loginRes.code);
// 2. 将 code 和加密数据发送到后端
this.sendPhoneNumberToServer(loginRes.code, e.detail.encryptedData, e.detail.iv);
} else {
console.error('wx.login failed:', loginRes.errMsg);
wx.showToast({
title: '微信登录失败',
icon: 'none'
});
}
},
fail: (err) => {
console.error('wx.login call failed:', err);
wx.showToast({
title: '微信登录接口调用失败',
icon: 'none'
});
}
});
},
sendPhoneNumberToServer: function (code, encryptedData, iv) {
wx.showLoading({
title: '登录中...',
mask: true
});
wx.request({
url: `${app.globalData.apiBaseUrl}/auth/wx/phoneLogin`, // 你的后端接口
method: 'POST',
header: {
'content-type': 'application/json'
},
data: {
code: code,
encryptedData: encryptedData,
iv: iv
},
success: (res) => {
wx.hideLoading();
if (res.statusCode === 200 && res.data && res.data.token) {
wx.setStorageSync('jwt_token', res.data.token);
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500,
success: () => {
// 登录成功后跳转到主页
setTimeout(() => {
wx.switchTab({
url: '/pages/index/index' // 你的主页路径,请根据实际情况修改
});
}, 1500);
}
});
} else {
wx.showToast({
title: res.data.message || '登录失败,请重试',
icon: 'none'
});
}
},
fail: (err) => {
wx.hideLoading();
console.error('请求后端接口失败', err);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
}
});
}
});
3. app.js (全局配置)
// app.js
App({
onLaunch() {
// ... 其他初始化逻辑
},
globalData: {
// 后端 API 地址,请替换为您的实际地址
apiBaseUrl: 'http://localhost:8080'
}
});
4. 授权配置
在小程序的 app.json 中,需要配置 requiredPrivateInfos 以声明需要获取的个人信息接口权限。
{
"pages": [
"pages/login/login",
"pages/index/index"
// ... 其他页面
],
"requiredPrivateInfos": [
"getPhoneNumber"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "我的应用",
"navigationBarTextStyle": "black"
},
"sitemapLocation": "sitemap.json"
}
复制
总结与注意事项:
- 安全性:
- AppSecret 保护:
AppSecret绝对不能泄露到前端。在后端使用 Jasypt 加密application.yml中的AppSecret是一个好习惯。 - JWT Secret 保护:
jwt.secret同样需要是复杂且安全的字符串,不应泄露。 - HTTPS: 生产环境必须使用 HTTPS,保护通信安全。
- AppSecret 保护:
- 用户体验:
- 自动注册: 如果小程序用户第一次使用手机号登录,自动注册可以大大简化流程。您可以为这些用户生成一个默认的用户名(例如
wx_手机号)和随机密码(加密后存储)。 - 错误提示: 完善的错误提示对用户很重要。
- 自动注册: 如果小程序用户第一次使用手机号登录,自动注册可以大大简化流程。您可以为这些用户生成一个默认的用户名(例如
- 日志: 后端打印详细日志,方便问题排查。
- 前端 Token 管理: 小程序端获取到 JWT 后,应存储在
wx.setStorageSync中,并在后续的请求中通过header: { 'Authorization': 'Bearer ' + token }发送给后端。 - 多个小程序/公众号: 如果您有多个小程序或公众号,
WxMaConfiguration需要能够管理多个WxMaConfig实例,通常通过AppID来区分。本示例已支持多配置,但默认只使用第一个配置。 WxMaConfigHolder:weixin-java-miniapp库在多小程序环境下,通常需要使用WxMaConfigHolder.set(appid)来指定当前线程操作的是哪个小程序的配置,以防止混淆。UserService.loadUserByUsername: Spring Security 的UserDetailsService接口通常只通过username加载用户。在您的小程序登录场景中,您可能会通过手机号查找用户,然后用该用户的username调用loadUserByUsername来生成UserDetails。- 数据库表: 确保您的
sys_user表有phone字段,并且phone字段最好是唯一的(加唯一索引)。 - 测试: 在开发过程中,确保您的微信小程序 AppID、AppSecret 配置正确,并且小程序开发工具可以正常调试。
这个方案应该能帮助您在现有 Spring Security + JWT 体系下实现微信小程序手机号登录功能。
3148

被折叠的 条评论
为什么被折叠?



