Spring Security 6.0 整合微信登录步骤

登录总流程

官方文档

注册好小程序之后可以拿到该小程序的 appIdappSecret

这里我们需要做的是:

  • 提供一个接口供前端调用,接口参数只有一个code;需要使用过滤器实现
  • 过滤器拿到code之后向微信服务器(接口)发送:appIdappSecretcode,获取session_keyopenId
  • openId即为用户的唯一标识,如果未注册则直接注册,并直接登录。
  • 本例中我们把常规用户和微信用户放在同一张表中,充分利用常规用户侧已经构建好的角色权限机制

具体实现

配置类

appIdappSecret需要从配置文件中读取,并提供一个方法生成登录请求的地址

/**
 * 小程序配置类
 */
@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方法尝试认证,此处为核心认证逻辑:

  1. 从token中取出code,发送请求获取openId
  2. 如果获取失败则抛出异常终止登录,如果获取成功则执行下一步
  3. 根据openId查询用户账号信息,如果用户未注册则自动执行注册
  4. 查询该账号持有的权限
  5. 构建UserDetails对象
  6. 创建一个已认证的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);
    }
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Security 6.0 目前还没有发布,不过它的开发正在进行中。目前最新的版本是 Spring Security 5.x,已经提供了 OAuth2 的支持。 要使用 Spring Security 5.x 中的 OAuth2,需要添加相应的依赖: ``` <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>5.x.x.RELEASE</version> </dependency> ``` 然后在 Spring Security 配置中配置 OAuth2 相关信息,例如: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authorizeRequests -> authorizeRequests .antMatchers("/login/**", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(); } @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository( ClientRegistration.withRegistrationId("google") .clientId("google-client-id") .clientSecret("google-client-secret") .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .scope("openid", "profile", "email") .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") .tokenUri("https://www.googleapis.com/oauth2/v4/token") .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") .userNameAttributeName(IdTokenClaimNames.SUB) .clientName("Google") .build() ); } } ``` 这里的配置中,我们使用了 InMemoryClientRegistrationRepository,这是一个轻量级的存储库,用于存储 OAuth2 客户端的注册信息。在上面的代码中我们注册了一个名为 "google" 的 OAuth2 客户端,并提供了必要的信息,例如:client id、client secret、authorization uri、token uri、user info uri 等。 当用户访问需要进行身份验证的资源时,Spring Security 会自动重定向到 OAuth2 服务提供商的登录页面,并让用户进行身份验证。如果身份验证成功,Spring Security 会获取访问令牌(access token),并将其用于访问受保护的资源。 以上是简单的介绍,具体的实现还需要根据实际的业务需求进行调整。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值