登录总流程
注册好小程序之后可以拿到该小程序的 appId
和appSecret
这里我们需要做的是:
- 提供一个接口供前端调用,接口参数只有一个code;需要使用过滤器实现
- 过滤器拿到code之后向微信服务器(接口)发送:
appId
、appSecret
、code
,获取session_key
,openId
openId
即为用户的唯一标识,如果未注册则直接注册,并直接登录。- 本例中我们把常规用户和微信用户放在同一张表中,充分利用常规用户侧已经构建好的角色权限机制
具体实现
配置类
appId
和appSecret
需要从配置文件中读取,并提供一个方法生成登录请求的地址
/**
* 小程序配置类
*/
@Configuration
@ConfigurationProperties(prefix = "wechat")
@Getter
@Setter
public class WechatConfig {
public static final String URL_TEMPLATE = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
String appId;
String appSecret;
/**
* 生成请求地址
* @param code code
* @return 请求地址
*/
public String obtainUrl(String code){
return String.format(URL_TEMPLATE,appId,appSecret,code);
}
}
创建Token
类似于用户名密码登录所使用的 UsernamePasswordAuthenticationToken
,微信登录也需要一个自己的Token,我们遵循官方的设计习惯,使用两个静态方法来分别创建未认证和已认证的Token。注意这里的principal
字段,登录成功后UsernamePasswordAuthenticationToken
会在这里保存UserDetails
,所以我们也应按此操作。
/**
* 微信登录认证Token
*
* @author : ginstone
* @version : v1.0.0
* @since : 2023/12/4 15:12
**/
@Getter
public class WechatAuthenticationToken extends AbstractAuthenticationToken {
private final String openId;
private final Object principal;
/**
* 未认证的Token
*
* @param openId openId
*/
public WechatAuthenticationToken(String openId) {
super(null);
this.openId = openId;
this.principal = openId;
setAuthenticated(false);
}
/**
* 已认证的token
*
* @param openId openId
* @param authorities 权限
*/
public WechatAuthenticationToken(String openId, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.openId = openId;
this.principal = principal;
setAuthenticated(true);
}
/**
* 未认证的Token
*
* @param openId openId
*/
public static WechatAuthenticationToken unauthenticated(String openId) {
return new WechatAuthenticationToken(openId);
}
/**
* 已认证的token
*
* @param openId openId
* @param authorities 权限
*/
public static WechatAuthenticationToken authenticated(String openId, Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WechatAuthenticationToken(openId, principal, authorities);
}
@Override
public Object getCredentials() {
return null;
}
创建过滤器
过滤器从请求中取出code参数,这里我们只简单地创建一个未认证的 WechatAuthenticationToken
,把code
放入, 并将其提交给 AuthenticationManager
/**
* 微信登录过滤器
*
* @author : ginstone
* @version : v1.0.0
* @since : 2023/12/4 14:25
**/
@Slf4j
@Component
public class WechatAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 微信登录路径
*/
public static final String WECHAT_LOGIN_PATH = "/wechat/login";
/**
* 允许的请求方法
*/
public static final String METHOD = "POST";
/**
* 参数名称
*/
public static final String PARAM_KEY = "code";
/**
* 路径匹配
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(WECHAT_LOGIN_PATH, METHOD);
public WechatAuthenticationFilter(AuthenticationManager authenticationManager, MyAuthenticationHandler authenticationHandler) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
setAuthenticationFailureHandler(authenticationHandler);
setAuthenticationSuccessHandler(authenticationHandler);
}
/**
* 从请求中取出code参数,向微信服务器发送请求,如果成功获取到openID,则创建一个未认证的 WechatAuthenticationToken , 并将其提交给 AuthenticationManager
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (!METHOD.equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
final String code = request.getParameter(PARAM_KEY);
if (ObjectUtils.isEmpty(code)) {
throw new AuthenticationServiceException("code 不允许为空");
}
// 创建一个未认证的token,放入code
final WechatAuthenticationToken token = WechatAuthenticationToken.unauthenticated(code);
// 设置details
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
// 将token提交给 AuthenticationManager
return this.getAuthenticationManager().authenticate(token);
}
}
然后我们还需要把过滤器添加到Spring Security
的过滤器链中,在你自定义的Security Config
类中的securityFilterChain
方法中添加第一句:
//微信登录过滤器
http.addFilterBefore(weChatAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
//原本的用户名密码登陆过滤器
http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
开发测试阶段可以先跳过获取openId
的步骤直接指定一个openId
测试后续操作是否能走通
创建登录验证器
AuthenticationManager
会根据验证器supports
方法的返回结果选择支持的验证器,调用验证器的authenticate
方法尝试认证,此处为核心认证逻辑:
- 从token中取出code,发送请求获取
openId
- 如果获取失败则抛出异常终止登录,如果获取成功则执行下一步
- 根据
openId
查询用户账号信息,如果用户未注册则自动执行注册 - 查询该账号持有的权限
- 构建
UserDetails
对象 - 创建一个已认证的Token,将
UserDetails
对象放入,返回token
/**
* 微信登录验证器
*
* @author : ginstone
* @version : v1.0.0
* @since : 2023/12/4 17:06
**/
@Component
@RequiredArgsConstructor
@Slf4j
public class WechatAuthenticationProvider implements AuthenticationProvider {
private final SystemUserService systemUserService;
private final WechatConfig wechatConfig;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取openId
final WechatAuthenticationToken token = (WechatAuthenticationToken) authentication;
final String code = token.getOpenId();
// 发送请求获取openId
final String openId = wechatLoginRequest(code, wechatConfig).getOpenId();
// 根据openId查询用户,如果不存在则注册
final SystemUser user = systemUserService.findOrRegByOpenId(openId);
//todo 权限提供者提供的权限
final Set<GrantedAuthority> authorities = ......
// 构建用户details对象,放入token中
final MyUserDetails userDetails = new MyUserDetails().with(user);
userDetails.setAuthorities(authorities);
// 返回一个已认证的Token
final WechatAuthenticationToken newToken = WechatAuthenticationToken.authenticated(openId, userDetails, authorities);
// 复制之前token的details
newToken.setDetails(token.getDetails());
return newToken;
}
/**
* 判断当前验证器是否支持给定的 authentication
*
* @param authentication authentication
* @return 是否支持
*/
@Override
public boolean supports(Class<?> authentication) {
return WechatAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 微信登录请求
*
* @param code wx.login()获取的code
* @param wechatConfig 微信配置
* @return 请求详情
*/
private static WechatLoginResponse wechatLoginRequest(String code, WechatConfig wechatConfig) {
// 请求地址
final String url = wechatConfig.obtainUrl(code);
log.info("url: {}", url);
// 发送登录请求
final String result = new RestTemplate().getForObject(url, String.class);
log.info(result);
try {
final WechatLoginResponse loginResponse = new ObjectMapper().readValue(result, WechatLoginResponse.class);
if (loginResponse == null) {
throw new AuthenticationServiceException("登录请求发送失败");
}
// 登录失败,报错
if (loginResponse.getCode() != null && loginResponse.getCode() != 0) {
log.warn(loginResponse.getErrMsg());
throw new AuthenticationServiceException(String.format("%d: %s", loginResponse.getCode(), loginResponse.getErrMsg()));
}
return loginResponse;
} catch (JsonProcessingException e) {
throw new AuthenticationServiceException("响应解析失败");
}
}
}
然后我们还需要把Provider
添加到AuthenticationManager
中
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*/
@Bean
public AuthenticationManager authenticationManager(DaoAuthenticationProvider daoAuthenticationProvider, WechatAuthenticationProvider wechatAuthenticationProvider) throws Exception {
return new ProviderManager(wechatAuthenticationProvider, daoAuthenticationProvider);
}