使用Spring Boot Security 实现多认证 手机号登录 微信扫码登录 微信扫码注册

Spring Boot 3.x
Spring Security 5.7
Spring Redis
MyBatis plus
前端 Vue

前言

公司 最近有个新项目 使用单点登录 sso
百度了一圈 也没怎么找到微信扫码注册的功能于是自己写

  • 需求就是 手机 + 密码登录
  • 微信扫码登录
  • 微信扫码注册

微信二维码 登录 和注册二合一 具体实现 稍后我会说

本教程将指导您如何使用Spring Boot和Spring Security 5.7来实现基于手机号密码登录的认证。通过本教程,您将学习如何配置Spring
Security,创建自定义的用户认证逻辑,并且使用手机号和密码进行登录。

手机号密码登录请求
微信扫码登录请求
请求
SecurityFilterChain
PhoneLoginConfigurer
手机号验证码校验
身份认证
生成认证信息
认证成功
WxQrLoginConfigurer
微信回调code处理

准备工作

添加核心依赖

在您的Spring Boot项目的pom.xml文件中添加Spring Security依赖:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
        <!--weixin-java-mp组件是一个基于Java语言开发的微信公众号开发工具包,是WxJava SDK在微信公众号场景的一个实现-->
<dependency>
	<groupId>com.github.binarywang</groupId>
	<artifactId>weixin-java-mp</artifactId>
	<version>${weixin.version}</version>
</dependency>
  • application.yml 部分代码
wxopen:
  openid: 开放平台网站openid
  appid: 公众号appid
  secret: 公众号凭据
  key: 公众号密钥
  #  扫码成功回调地址 这是你的回调接口 /code 文章后面会说
  redirect_url: https://www.xxxx.com/code

配置实体类

@Configuration
public class WxMpServiceConfiguration {


    @Value("${wxopen.appid}")
    private String appId;
    @Value("${wxopen.openid}")
    private String openid;
    @Value("${wxopen.secret}")
    private String appSecret;
    @Value("${wxopen.redirect_url}")
    private String redirectUrl;
    /**
     * 定义WxMpService bean
     */
    @Bean
    public WxMpService wxMpService() {
        WxMpService wxMpService = new WxMpServiceImpl();
        WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
        config.setAppId(openid);
         config.setOauth2redirectUri(redirectUrl);
        config.setSecret(appSecret);
        wxMpService.setWxMpConfigStorage(config);
        return wxMpService;
    }
}


}

开始回归正题 实现基于手机号密码登录

获取微信授权二维码 登录 注册 我都使用一个二维码 无非就是加一个 标识来区分是 登录 还是注册 (mode)

/**
 * @Desc 获取微信授权二维码 此接口只接收前端的请求(监听IP白名单)
 * @Author Likefr
 * @param mode (login) 登录 | (register) 注册
 * @Date 2024/03/01 11:02
 */
@GetMapping("/qr/{mode}")
public ResultBean getAuthQr(@PathVariable("mode") String mode) {
    if (mode == null) {
        return ResultBean.error(ResultBeanEnum.BIND_ERROR);
    }
    Map<String, Object> map = new HashMap<>();
    map.put("appid", weChatConfig.getOpenId());
    map.put("scope", "snsapi_login");
    String uuid = UUID.randomUUID().toString();
    String backUrl = "https://xxxxx.com/wechat/code?handlerType=" + mode + "&uuid=" + uuid;
    map.put("redirect_uri", backUrl);
    map.put("state", uuid);
    return ResultBean.success(map);
}

可能会问 backUrl 这个地址是哪个地址? 其实就是一个 我们springboot 的一个接口

@RequestMapping(value = "/code", method = {RequestMethod.GET, RequestMethod.POST})

这个接口 放在下面具体 往下滑 !


前端 发送 @GetMapping(“/qr/{mode}”) 接口 来获取二维码
比如 :

  • 我请求 /qr/login 我就能获取登录的二维码
  • 我请求 /qr/register 我就能获取注册的二维码

我这边贴上部分代码 前端需要引入才能 使用 new WxLogin

      <script src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
      <script src="https://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
// springboot 后端接口返回 (@GetMapping("/qr/{mode}"))
data.data =  return ResultBean.success(map);

    var obj = new WxLogin({
          self_redirect: false,
          id: 'weixinLogin', // 需要显示的容器id
          appid: data.data.appid, // 公众号appid wx*******
          scope: data.data.scope, // 网页默认即可
          redirect_uri: data.data.redirect_uri, // 授权成功后回调的url
          state: data.data.state, // 可设置为简单的随机数加session用来校验
          style: 'black', // 提供"black"、"white"可选。二维码的样式
          href: '' // 外部css文件url,需要https
        })
        return
      }
          <div id="weixinLogin"></div>

这样 前端就会显示二维码

在这里插入图片描述
然后就是微信扫码成功后重定向 也就是调用我们这个接口的回调

tips 还记得我们刚才 前端 调用的 @GetMapping(“/qr/{mode}”) 接口参数吗
mode 这个参数 就是用来区分 我是用来 登录 还是注册(handlerType)

这点很重要 !!!

/**
 * @Desc 该接口只接收微信回调请求获取code
 * @Author Likefr
 * @Date 2024/2/28 16:05
 */
@RequestMapping(value = "/code", method = {RequestMethod.GET, RequestMethod.POST})
public void getCallBackCode(HttpServletRequest request, HttpServletResponse response) throws IOException, WxErrorException {
    String code = request.getParameter("code");                //获取code
    String handlerType = request.getParameter("handlerType");  //获取二维码类型
    // 登录 
    if ("login".equals(handlerType)) {
        response.sendRedirect("https://www.xxxx.com/#/index?uuid=" + code);
        // 注册
    } else if ("register".equals(handlerType)) {
        response.sendRedirect("https://www.xxxx.com/login/#/?uuid=" + code);
    }
}

如果是login 我跳到前端首页

   if ("login".equals(handlerType)) {
            response.sendRedirect("https://www.xxxx.com/#/index?uuid=" + code);
        } else if ("register".equals(handlerType)) {
            response.sendRedirect(""https://www.xxxx.com/login/#/?uuid=" + code);
        }

这段代码 就是微信帮我们跳转到前端 的具体某个页面
如果是扫码登录
response.sendRedirect("https://www.xxxx.com/#/login?uuid=" + code);

前端 vue框架 created 钩子函数 里面判断 链接 uuid=" + code 是否存在code 存在则 发送/wechat/login接口

    // spring security 拦截 POST /wechat/login 请求
    RequestMatcher matcher = new AntPathRequestMatcher("/wechat/login", "POST", true);

然后经过 WxQrAuthenticationFilter WxQrAuthenticationFilter 获取 刚才 链接上uuid 字段 的code值 获取到 uuid后 封装一个 Authentication authentication = new WxQrAuthenticationToken(userForm.getUuid()); 对象

在 调用 WxQrCodeAuthenticationProvider 最后认证

以下是我的 security 微信扫码登录认证器

/**
 * @author Likefr
 * 基于用户名(手机号)、密码、验证码的登录拦截器配置类
 */
public class WxQrLoginConfigurer extends AbstractHttpConfigurer<WxQrLoginConfigurer, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) {
        // 拦截 POST /login 请求
        RequestMatcher matcher = new AntPathRequestMatcher("/wechat/login", "POST", true);
        SecurityUserDetailsService userDetailService = builder.getSharedObject(ApplicationContext.class).getBean(SecurityUserDetailsService.class);
        RedisTemplate redisTemplate = builder.getSharedObject(ApplicationContext.class).getBean(RedisTemplate.class);
        WxMpService wxMpService = builder.getSharedObject(ApplicationContext.class).getBean(WxMpService.class);
        AuthenticationManager localAuthManager = builder.getSharedObject(AuthenticationManager.class);

        WxQrAuthenticationFilter filter = new WxQrAuthenticationFilter(matcher, localAuthManager);
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(userDetailService, redisTemplate));
        filter.setAuthenticationFailureHandler(new LoginFailHandler());
        // 务必注意这里与配置类中声明的先后顺序
        builder.authenticationProvider(new WxQrCodeAuthenticationProvider(userDetailService, redisTemplate, wxMpService))
                .addFilterBefore(filter, AuthenticationTokenFilter.class);
    }
}


@Slf4j
public class WxQrCodeAuthenticationProvider implements AuthenticationProvider {

    private SecurityUserDetailsService userDetailsService;

    private RedisTemplate<String, Object> redisTemplate;
    private WxMpService wxMpService;

    public WxQrCodeAuthenticationProvider(SecurityUserDetailsService userService, RedisTemplate redisTemplate, WxMpService wxMpService) {
        this.userDetailsService = userService;
        this.redisTemplate = redisTemplate;
        this.wxMpService = wxMpService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        WxQrAuthenticationToken authenticationToken = (WxQrAuthenticationToken) authentication;
        // 获取前端的
        String uuid = authenticationToken.getUuid();
        // 根据uuid 获取微信用户 (核心)
        SecutityUser userDetails = checkUuid(uuid);

        if (Objects.isNull(userDetails)) {
            throw new InternalAuthenticationServiceException(" 无效的 uuid!");
        }
        // 用户状态校验
        if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {
            throw new LockedException("用户已禁用,请联系管理员启用");
        }
        WxQrAuthenticationToken authenticationResult = new WxQrAuthenticationToken(userDetails, uuid, new ArrayList<>());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return WxQrAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private SecutityUser checkUuid(String uuid) {
        WxOAuth2AccessToken accessToken = null;
        WxOAuth2UserInfo userInfo;
        try {
            accessToken = wxMpService.getOAuth2Service().getAccessToken(uuid);
            userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken, "zh_CN");
        } catch (WxErrorException e) {
            throw new CredentialsExpiredException(e.getMessage().toString());
        }
//      根据微信用户id 查找你数据库的用户 这边我不贴代码了 很简单 
        SecutityUser userDetails = userDetailsService.loadUserByUsername(userInfo.getOpenid());
        return userDetails;
    }

}


/**
 * 二维码登录拦截器
 */
public class WxQrAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public WxQrAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
        super(requiresAuthenticationRequestMatcher, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        LoginVo userForm = HttpRequestUtil.getBodyJson(request);
        logger.info(userForm.toString());
        Authentication authentication = new WxQrAuthenticationToken(userForm.getUuid());
        // authenticate 会执行 SecutityUserDetailsService
        return this.getAuthenticationManager().authenticate(authentication);
    }
}

基于手机号 + 密码的认证登录


/**
 * @author Likefr
 * 基于用户名(手机号)、验证码的登录拦截器配置类
 */
public class PhoneLoginConfigurer extends AbstractHttpConfigurer<PhoneLoginConfigurer, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) {
        // 拦截 POST /login 请求
        RequestMatcher matcher = new AntPathRequestMatcher("/phone/doLogin", "POST", true);
        SecurityUserDetailsService userDetailService = builder.getSharedObject(ApplicationContext.class).getBean(SecurityUserDetailsService.class);
        RedisTemplate redisTemplate = builder.getSharedObject(ApplicationContext.class).getBean(RedisTemplate.class);
        AuthenticationManager localAuthManager = builder.getSharedObject(AuthenticationManager.class);

        PhoneAuthenticationFilter filter = new PhoneAuthenticationFilter(matcher, localAuthManager);
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(userDetailService, redisTemplate));
        filter.setAuthenticationFailureHandler(new LoginFailHandler());
        // 委托认证autheticate AuthenticationProvider
        builder.authenticationProvider(new PhoneAuthenticationProvider(userDetailService, redisTemplate))
                .addFilterBefore(filter, AuthenticationTokenFilter.class);
    }
}


/**
 * @author Likefr
 * 基于用户名(手机号)、密码的认证处理器
 */
@Slf4j
public class PhoneAuthenticationProvider implements AuthenticationProvider {

    private final SecurityUserDetailsService userDetailService;
    private RedisTemplate<String, Object> redisTemplate;

    public PhoneAuthenticationProvider(SecurityUserDetailsService userDetailService, RedisTemplate redisTemplate) {
        this.userDetailService = userDetailService;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 验证主逻辑
     */
    /*
   provider = class ProviderManager implements AuthenticationManager
    	result = provider.authenticate(authentication);
    * */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        PhoneAuthenticationToken authToken = (PhoneAuthenticationToken) authentication;

        if (authToken.getPhone() == null) {
            throw new UsernameNotFoundException("未输入手机号");
        }
        if (Objects.isNull(authToken.getCredentials())) {
            throw new BadCredentialsException("密码不能为空");
        }
        // 加载用户详情
//        log.info("authToken.getCredentials() {} {}", authToken.getCredentials(), authToken.getPhone());

        SecutityUser userDetails = userDetailService.loadUserByUsername(authToken.getPhone());
        if (Objects.isNull(userDetails.getSysUser().getPassword())) {
            throw new BadCredentialsException("当前用户未设置密码");
        }
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        if (!bCryptPasswordEncoder.matches(authToken.getCredentials(), userDetails.getSysUser().getPassword())) {
            throw new BadCredentialsException("用户名或密码错误,请重新输入");
        }
        // 检查用户状态
        if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {
            throw new LockedException("用户处于禁用状态,请联系管理员启用");
        }

        // 生成 JWT 并存储
        return new PhoneAuthenticationToken(userDetails, authToken.getCredentials(), authToken.getAuthorities());
    }


    /**
     * 当类型为PasswordAuthenticationToken的认证实体进入时才走此Provider
     */
//
    @Override
    public boolean supports(Class<?> authentication) {
        return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
    }

/**
 * @author Likefr
 * 用户名密码登录拦截器
 * 主要处理两个事情 1 @params request 提取前端 提交的表单信息
 * 2 然后封装成 基于 手机号 验证码 实现的Authentication 对象
 * retuerns Authentication
 */
public class PhoneAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public PhoneAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
        super(requiresAuthenticationRequestMatcher, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        LoginVo userForm = HttpRequestUtil.getBodyJson(request);
        Authentication authentication = new PhoneAuthenticationToken(userForm.getNickname(), userForm.getPassword());
        //  authenticate委托认证 交由 DaoAuthenticationProvider
        return this.getAuthenticationManager().authenticate(authentication);
    }
}

增加 Token 拦截器配置

/**
 * Token拦截器配置类
 * TokenAuthenticationFilter 负责处理我们携带了jwt token的请求。认证工作主要由它负责。
 */
public class TokenAuthenticationConfigurer extends AbstractHttpConfigurer<TokenAuthenticationConfigurer, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) {
        RedisTemplate RedisTemplate = builder.getSharedObject(ApplicationContext.class).getBean(RedisTemplate.class);
        AccessDeniedHandler accessDeniedHandler  = builder.getSharedObject(ApplicationContext.class).getBean(AccessDeniedHandler.class);
        builder.addFilterBefore(new AuthenticationTokenFilter(RedisTemplate, accessDeniedHandler), UsernamePasswordAuthenticationFilter.class);
    }
}

JWT 拦截处理实现


/**
 * JWT Token认证拦截器 每次请求接口会先解析jwt
 * 用户 判断是否存在token
 * token 是否有效
 * token 是否过期
 */
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    // token 存在redis 里边 不存在则已退出登录
    private RedisTemplate redisTemplate;

    public AuthenticationTokenFilter(RedisTemplate redisTemplate,
                                     AccessDeniedHandler accessDeniedHandler) {
        this.redisTemplate = redisTemplate;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    private AccessDeniedHandler accessDeniedHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        // 未登录 如果字符串不为 null 值,并且不含有空白字符序列,并且字符序列的长度大于 0 ,则返回 true
        if (!StringUtils.hasText(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        token = token.replace("bearer ", "");

        // 判断jwt 是否过期
        Boolean expire = JWTUtils.isExpire(token);
        if (expire == null) {
            accessDeniedHandler.handle(request, response, new AccessDeniedException(ResultBeanEnum.JWT_ERROR.getMeassage()));
            return;
        }
        if (expire) {
            // token 过期
            filterChain.doFilter(request, response);
            return;
        }
        Boolean redisJwtExists = redisTemplate.hasKey("loginToken:" + token);
        SecutityUser userPojo = JWTUtils.checkToken(token);
        if (userPojo != null && redisJwtExists) {
            // 将解析的jwt 写入上下文 以供全局使用
            PhoneAuthenticationToken authenticationToken = new PhoneAuthenticationToken(userPojo, token, userPojo.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            // todo 实现jwt 无感续签
//      redisCacheUtil.setExpire(TokenConstant.TOKEN_REDIS_PREFIX + token, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);
        }

        filterChain.doFilter(request, response);
    }
}


接下来就是最重要的配置 security了 将我们前面的拦截器 配置进去


/**
 * @version: java version 17
 * @Author: Likefr
 * @description:
 * @date: 2024-01-20 11:44
 */

@Configuration

/*
* 从Spring Security 4.0开始,不推荐使用@EnableWebMvcSecurity 。 replace是@EnableWebSecurity将决定添加基于类path的Spring MVCfunction。
为了使Spring Security与Spring MVC集成,将 @EnableWebSecurity 注释 添加 到您的configuration中。
* */
@EnableWebSecurity
public class WebSecutityConfig {

    @Autowired
    private LogoutSuccessHanlder logoutHandler;

    @Autowired
    private LoginFailHandler loginFailHandler;

    @Autowired
    AuthenticationExceptionHandler authenticationFailHandler;


    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //关闭csrf
        http.csrf().disable()
                //自定义登录接口 禁用 FormLoginConfigurer 默认登录实现
                .formLogin().disable()
                .exceptionHandling().authenticationEntryPoint(authenticationFailHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER).and()
                .authorizeHttpRequests(authorize -> authorize
                        // 放行手机号密码登录接口
                        .requestMatchers("/phone/doLogin").permitAll()
                        // 放行用户注册接口
                        .requestMatchers("/sysUser/register").permitAll()
                        // 微信扫码回调(这个很重要)
                        .requestMatchers("/code/**").permitAll()
                        .requestMatchers("/wechat/**").permitAll()
                        .anyRequest().authenticated())
                .formLogin().failureHandler(loginFailHandler).and()
                .logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).and()
                // 务必注意这里的先后顺序,否则会报NULL异常
                // token 拦截 我使用的是jwt
                .apply(new TokenAuthenticationConfigurer()).and()
                // 手机号登录拦截器
                .apply(new PhoneLoginConfigurer()).and()
                // 微信扫码登录拦截器
                .apply(new WxQrLoginConfigurer());
        return http.build();
    }
}

大致说下上面代码

需要注意的是放行登录 注册 接口 还有获取微信二维码的接口 如果不放行 你的登录或者注册 没法访问 (
没登陆当然无法访问了,熟悉security的都知道)

                // token 拦截 我使用的是jwt
                .apply(new TokenAuthenticationConfigurer()).and()
                        // 手机号登录拦截器
                        .apply(new PhoneLoginConfigurer()).and()
                        // 微信扫码登录拦截器
                        .apply(new WxQrLoginConfigurer());

这三行代码是在配置 Spring Security 的过滤器链中应用了三个自定义的配置器(configurer)。这些配置器定义了特定的认证方式或者处理特定类型的认证请求
我简单说下:

  • .apply(new TokenAuthenticationConfigurer()): 这一行代码声明了一个名为 TokenAuthenticationConfigurer
    的配置器,这个配置器我实现了拦截基于令牌(JWT)的身份验证

  • .apply(new PhoneLoginConfigurer()): 这一行代码声明了一个名为 PhoneLoginConfigurer 的配置器,准确的说是手机号
    密码登录这个配置器用于配置手机登录认证方式,包含手机号 + 密码登录

  • .apply(new WxQrLoginConfigurer()): 这一行代码应用了一个名为 WxQrLoginConfigurer 的配置器。根据名称应该可以知道,是一个前端扫码微信二维码登录,
    这个配置了微信扫码登录认证方式,涉及对接微信授权。

这三个配置器的先后顺序非常重要,因为它们可能会相互依赖或者有先后执行的逻辑,说这里的顺序可能影响到程序的正确性。如果这些配置器之间有先后顺序的要求,而没有按照要求配置,就有可能出现空指针异常(NULL
异常)或其他配置错误。在使用这些配置器时务必注意它们的先后顺序

再就是微信扫码注册实现 还记得 我们前端调用的这个接口吗?

@GetMapping("/qr/{mode}")

当mode = register时 我会重定向到前端登录页

response.sendRedirect("https://www.xxxx.com/login/#/?uuid=" + code);

就是我会在注册页Login.vue created 钩子函数里面
判断 ?uuid=" + code 是否存在 存在的话 就说明是用户正在注册 让用户 输入 手机号 密码 和验证码注册
注册接口 如下

具体参数,其实无非就是你注册的手机号 还有密码 这个根据你的业务去写
loginVo 实体类包含uuid这个字段 这个u id的值其实就是微信扫码登录成功返回 code
** response.sendRedirect(“https://www.xxxx.com/login/#/?uuid=” + code);**
所以前端要解析出 uuid 并在注册的时候把这个值传过来,获取微信用户


    public ResultBean addUser(LoginVo loginVo) {
		// 手机验证码是否为空
        if (StringUtils.isEmpty(loginVo.getCode())) {
            return ResultBean.error(ResultBeanEnum.CODE_NOTNULL_ERROR); // 验证码为空
        }
		// 校验验证码
        String redisCode = (String) redisTemplate.opsForValue().get(RedisKeyConstant.SMSCODE.cacheKey + loginVo.getPhone());
        if (!loginVo.getCode().equals(redisCode)) {
            return ResultBean.error(ResultBeanEnum.CODE_ERROR);
        }

        QueryWrapper<SysUser> wrapper = new QueryWrapper<SysUser>();
		// 判断手机号是否注册过
        wrapper.eq("phone", loginVo.getPhone());
        long count = count(wrapper);
        if (count > 0) {
            return ResultBean.error(ResultBeanEnum.PHONE_OTHER_ACCOUNTS_ERROR);
        }

        // 获取微信id
        WxOAuth2AccessToken accessToken = null;
        WxOAuth2UserInfo userInfo;
        try {
            log.info("loginVo.getUuid():{}", loginVo.getUuid());
            accessToken = wxMpService.getOAuth2Service().getAccessToken(loginVo.getUuid());
//            log.info("accessToken- {}", accessToken);
            userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken, "zh_CN");
        } catch (WxErrorException e) {
            throw new CredentialsExpiredException(e.getMessage().toString());
        }
        wrapper = new QueryWrapper<SysUser>();
        wrapper.eq("openid", userInfo.getOpenid());
        // 判断微信是否已经绑定了其他手机号
        long wxCount = count(wrapper);
        if (wxCount > 0) {
            return ResultBean.error(ResultBeanEnum.WECHAT_OTHER_ACCOUNTS_ERROR);
        }


        SysUser sysUser = new SysUser();
        sysUser.setNickname(loginVo.getPhone());
        sysUser.setPhone(loginVo.getPhone());
        sysUser.setOpenid(userInfo.getOpenid());
        sysUser.setCreateTime(LocalDateTime.now());
        sysUser.setState(true);
        if (!ObjectUtils.isEmpty(loginVo.getPassword())) {
            String password = new BCryptPasswordEncoder().encode(loginVo.getPassword());
            sysUser.setPassword(password);
        }
        boolean save = save(sysUser);
        if (save) {
        // 注册成功
            return ResultBean.success();
        }
        return ResultBean.error(ResultBeanEnum.USER_ADD_ERROR);
    }
  • 31
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
要在Spring Security使用微信扫码登录,您需要完成以下步骤: 1. 在微信开放平台上创建应用程序并配置授权域名。 2. 在应用程序中创建一个扫码登录的方式,获取重定向URL和state参数。 3. 在Spring Security配置中添加一个OAuth2客户端,包括客户端ID、客户端密钥、授权域和重定向URL。 4. 创建一个自定义的OAuth2认证过滤器,用于处理微信回调请求。 以下是一个简单的示例,演示如何在Spring Security使用微信扫码登录: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private OAuth2AuthorizedClientService authorizedClientService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login/**").permitAll() .anyRequest().authenticated() .and() .oauth2Login() .redirectionEndpoint() .baseUri("/login/oauth2/code/wechat") .and() .userInfoEndpoint() .userService(wechatOAuth2UserService()) .and() .authorizedClientService(authorizedClientService) .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .permitAll(); } @Bean public OAuth2UserService<OAuth2UserRequest, OAuth2User> wechatOAuth2UserService() { return new WechatOAuth2UserService(); } private static class WechatOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // 获取微信用户信息 Map<String, Object> userInfo = userRequest.getAdditionalParameters(); String openid = (String) userInfo.get("openid"); String nickname = (String) userInfo.get("nickname"); String avatarUrl = (String) userInfo.get("headimgurl"); // 构造OAuth2User对象 return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), userInfo, "openid"); } } @Bean public OAuth2AuthorizedClientRepository authorizedClientRepository() { return new HttpSessionOAuth2AuthorizedClientRepository(); } @Bean public OAuth2AuthorizedClientService authorizedClientService( OAuth2ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { return new DefaultOAuth2AuthorizedClientService(clientRegistrationRepository, authorizedClientRepository); } @Bean public ClientRegistrationRepository clientRegistrationRepository() { // 配置微信OAuth2客户端 ClientRegistration wechat = ClientRegistration.withRegistrationId("wechat") .clientId("your-client-id") .clientSecret("your-client-secret") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") .scope("snsapi_login") .authorizationUri("https://open.weixin.qq.com/connect/qrconnect") .tokenUri("https://api.weixin.qq.com/sns/oauth2/access_token") .userInfoUri("https://api.weixin.qq.com/sns/userinfo") .userNameAttributeName("openid") .clientName("Wechat") .build(); return new InMemoryClientRegistrationRepository(wechat); } @Bean public OAuth2AuthorizationRequestRedirectFilter oauth2AuthorizationRequestRedirectFilter( OAuth2AuthorizedClientService authorizedClientService) { // 自定义OAuth2认证过滤器,用于处理微信回调请求 OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter( clientRegistrationRepository(), authorizedClientRepository()); filter.setAuthorizationRequestRepository( new HttpSessionOAuth2AuthorizationRequestRepository()); filter.setAuthorizationUri("https://open.weixin.qq.com/connect/qrconnect"); filter.setPrincipalExtractor(new OAuth2AccessTokenResponseClientAdapter( new DefaultOAuth2AccessTokenResponseClient()).getPrincipalExtractor()); filter.setAuthorizationClientService(authorizedClientService); return filter; } } ``` 您需要替换上述代码中的`your-client-id`和`your-client-secret`为您的微信应用程序的客户端ID和客户端密钥。 此外,您还需要编写一个控制器来处理登录页面和回调URL: ``` @Controller public class LoginController { @Autowired private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/login") public String login(HttpServletRequest request, Model model) { // 创建扫码登录URL和state参数 String redirectUrl = "http://" + request.getServerName() + ":" + request.getServerPort() + "/login/oauth2/code/wechat"; String state = UUID.randomUUID().toString().replaceAll("-", ""); // 将state参数存储在会话中 request.getSession().setAttribute("state", state); // 构造微信扫码登录URL String qrCodeUrl = "https://open.weixin.qq.com/connect/qrconnect?appid=your-app-id&redirect_uri=" + URLEncoder.encode(redirectUrl, "UTF-8") + "&response_type=code&scope=snsapi_login&state=" + state + "#wechat_redirect"; model.addAttribute("qrCodeUrl", qrCodeUrl); return "login"; } @GetMapping("/login/oauth2/code/wechat") public String loginCallback(HttpServletRequest request, HttpServletResponse response, @RequestParam("code") String code, @RequestParam("state") String state, Authentication authentication) throws IOException { // 验证state参数是否匹配 String sessionState = (String) request.getSession().getAttribute("state"); if (!state.equals(sessionState)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid state parameter"); return null; } // 使用OAuth2AuthorizedClientService获取访问令牌 OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("wechat", authentication.getName()); String accessToken = authorizedClient.getAccessToken().getTokenValue(); // 处理用户登录逻辑 // ... return "redirect:/"; } } ``` 在`/login`请求中,我们构造了一个微信扫码登录的URL,并将其添加到模型中返回给用户。在`/login/oauth2/code/wechat`请求中,我们验证了回调请求中的state参数,并使用`OAuth2AuthorizedClientService`获取访问令牌,然后处理用户登录逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值