微信小程序登录流程
概述
微信小程序登录和普通用户名密码登录不一样。用户不是把密码交给我们,而是小程序先向微信拿到一个临时code,再把code交给自己的服务端,服务端用code去微信服务器换openid和session_key。
我们自己的系统不应该直接把微信session_key返回给前端。更稳的做法是:服务端确认openid以后,创建或查询本地用户,再签发自己的JWT。
小程序 wx.login
|
v
拿到 code
|
v
后端调用微信 jscode2session
|
v
拿到 openid / unionid / session_key
|
v
创建或查询本地用户
|
v
签发自己的 JWT
这样做以后,后续接口就不需要每次都访问微信。微信负责第一次身份入口,我们自己的JWT负责业务系统登录态。
小程序端
小程序端调用wx.login获取code,再提交给后端。
wx.login({
success(res) {
if (!res.code) {
return;
}
wx.request({
url: 'https://api.zhouzhou.net/auth/wechat/login',
method: 'POST',
data: {
code: res.code
},
success(loginRes) {
wx.setStorageSync('accessToken', loginRes.data.accessToken);
}
});
}
});
后续请求带上自己的JWT:
const accessToken = wx.getStorageSync('accessToken');
wx.request({
url: 'https://api.zhouzhou.net/api/profile',
header: {
Authorization: `Bearer ${accessToken}`
}
});
小程序端只保存我们自己签发的Token,不保存微信session_key。
登录请求
后端可以先定义一个登录请求对象。
public record WechatLoginRequest(
String code
) {
}
返回值也保持简单:
public record TokenResponse(
String accessToken,
String tokenType,
long expiresIn
) {
}
tokenType一般返回Bearer,前端请求接口时就能拼出标准的Authorization请求头。
微信会话对象
调用微信接口以后,会拿到openid、session_key,如果小程序绑定了开放平台,还可能拿到unionid。
public record WechatSession(
String openid,
String unionid,
String sessionKey
) {
}
openid是用户在当前小程序下的唯一标识,unionid是同一开放平台账号下的统一标识。只做一个小程序时,用openid就够了。如果后面要打通公众号、App、多个小程序,就要考虑unionid。
调用微信接口
微信登录服务负责拿code换会话。
@Service
public class WechatMiniAppService {
private final RestClient restClient;
private final WechatProperties properties;
public WechatMiniAppService(RestClient.Builder builder,
WechatProperties properties) {
this.restClient = builder
.baseUrl("https://api.weixin.qq.com")
.build();
this.properties = properties;
}
public WechatSession codeToSession(String code) {
WechatCode2SessionResponse response = restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/sns/jscode2session")
.queryParam("appid", properties.appId())
.queryParam("secret", properties.appSecret())
.queryParam("js_code", code)
.queryParam("grant_type", "authorization_code")
.build())
.retrieve()
.body(WechatCode2SessionResponse.class);
if (response == null || response.errcode() != null) {
throw new BadCredentialsException("微信登录失败");
}
return new WechatSession(
response.openid(),
response.unionid(),
response.sessionKey()
);
}
}
微信返回对象可以这样定义:
public record WechatCode2SessionResponse(
String openid,
String unionid,
@JsonProperty("session_key")
String sessionKey,
Integer errcode,
String errmsg
) {
}
这里使用RestClient是因为新版本Spring里它比RestTemplate更适合新项目。如果项目还在Spring Boot 2,可以继续使用RestTemplate或WebClient。
配置对象:
@ConfigurationProperties(prefix = "app.wechat.miniapp")
public record WechatProperties(
String appId,
String appSecret
) {
}
配置文件:
app:
wechat:
miniapp:
app-id: ${WECHAT_MINIAPP_APP_ID}
app-secret: ${WECHAT_MINIAPP_APP_SECRET}
appSecret不要写死到仓库里。放到环境变量、配置中心或服务器密钥管理里会更安全。
OpenID登录Token
原来的设计里有一个OpenIDToken基类,这个思路可以保留。小程序类Token不应该被直接标记为已认证,它只是认证过程中的输入,最终要转换成系统自己的登录态。
public abstract class OpenIDToken extends AbstractAuthenticationToken {
protected OpenIDToken() {
super(null);
super.setAuthenticated(false);
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("不能直接信任小程序登录Token");
}
super.setAuthenticated(false);
}
}
微信小程序Token可以这样写:
public class WechatMiniAppAuthenticationToken extends OpenIDToken {
private final String code;
public WechatMiniAppAuthenticationToken(String code) {
this.code = code;
}
@Override
public Object getCredentials() {
return code;
}
@Override
public Object getPrincipal() {
return null;
}
}
这段Token只代表“用户提交了一个微信code”,还不代表用户已经登录。
AuthenticationProvider
真正的认证逻辑放到AuthenticationProvider中。它负责调用微信接口、查询本地用户、组装已认证的Authentication。
@Component
public class WechatMiniAppAuthenticationProvider implements AuthenticationProvider {
private final WechatMiniAppService wechatMiniAppService;
private final UserService userService;
public WechatMiniAppAuthenticationProvider(WechatMiniAppService wechatMiniAppService,
UserService userService) {
this.wechatMiniAppService = wechatMiniAppService;
this.userService = userService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String code = (String) authentication.getCredentials();
WechatSession session = wechatMiniAppService.codeToSession(code);
LoginUser user = userService.findOrCreateByWechatOpenid(session.openid(), session.unionid());
return new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return WechatMiniAppAuthenticationToken.class.isAssignableFrom(authentication);
}
}
findOrCreateByWechatOpenid是业务方法。第一次登录时创建用户,后续登录时直接查用户。如果业务需要绑定手机号,可以先创建一个半注册用户,等用户授权手机号后再补全资料。
登录接口
Controller里只做流程编排,不直接处理微信细节。
@RestController
@RequestMapping("/auth/wechat")
public class WechatAuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenService jwtTokenService;
public WechatAuthController(AuthenticationManager authenticationManager,
JwtTokenService jwtTokenService) {
this.authenticationManager = authenticationManager;
this.jwtTokenService = jwtTokenService;
}
@PostMapping("/login")
public TokenResponse login(@RequestBody WechatLoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new WechatMiniAppAuthenticationToken(request.code())
);
LoginUser user = (LoginUser) authentication.getPrincipal();
String accessToken = jwtTokenService.createAccessToken(user);
return new TokenResponse(accessToken, "Bearer", 3600);
}
}
这段代码和普通用户名密码登录很像。区别只是用户名密码Token换成了微信小程序Token。
配置AuthenticationManager
如果项目里有多个认证方式,可以把不同AuthenticationProvider注册进去。
@Bean
AuthenticationManager authenticationManager(WechatMiniAppAuthenticationProvider wechatProvider,
DaoAuthenticationProvider daoProvider) {
return new ProviderManager(List.of(wechatProvider, daoProvider));
}
这样用户名密码登录、小程序登录、后续可能的支付宝登录,都可以走同一套AuthenticationManager。
安全配置
微信登录接口需要放行,其他业务接口仍然需要JWT。
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST, "/auth/wechat/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
登录接口放行,不代表它不安全。它仍然会调用微信接口校验code,并且code本身是临时的,不能重复长期使用。
需要注意的点
微信code只能作为登录凭证,不要当用户ID保存。真正能稳定识别用户的是openid或unionid。
session_key不能返回给前端,也不应该放到JWT里。它是解密微信敏感数据的凭证,只应该保存在服务端。
如果需要解密手机号,要校验微信返回数据的签名和水印信息,确认appid和当前小程序一致。
如果登录接口被频繁刷,要加限流。微信接口不是你自己的数据库,外部依赖要保护好。
小结
微信小程序登录的关键点是边界清楚:微信负责确认用户在微信体系里的身份,我们自己的系统负责维护本地用户和业务Token。
不要让整个业务系统依赖微信会话。换到自己的JWT以后,后面的接口就能和普通API一样处理认证和权限。
作者: 舟哥
链接:https://www.b919p4.com
来源: B919P4
本文原创发布于B919P4,©著作权归作者所有,转载请注明出处,谢谢合作!