认证授权-2

Author Avatar
ciky 11月 12,2024
  • 在其它设备中阅读本文章
  • 点击生成二维码

认证授权

    ├── 5 用户认证
    │   ├── 5.1 认证流程
    │	│   │
    │   ├── 5.2 连接数据库认证
    │   │   ├── (1) 原来的方法       
    │   │   ├── (2) 查询用户原理
    │   │   ├── (3) 实现数据库认证
    │   │   ├── (4) 修改加密方式
    │	│   │
    │   ├── 5.3 扩展用户信息
    │   │   ├── (1) 目前问题
    │   │   ├── (2) 方案一   
    │   │   ├── (3) 方案二   	
    │	│   │
    │   ├── 5.4 资源服务获取用户信息
    │   │   ├── (1) 工具类
    │   │   ├── (2) 获取用户信息  
    │	│   │
    │   ├── 5.5 统一认证入口
    │   │   ├── (1) 统一认证参数       
    │   │   ├── (2) 修改自定义UserDetailService
    │   │   ├── (3) 自定义DaoAuthenticationProvider
    │   │   ├── (4) 修改WebSecurityConfig
    │   │   ├── (5) 统一认证接口--策略模式
    │   │   ├── (6) 修改自定义UserDetailService
    │	│   │
    │   ├── 5.6 账号密码认证
    
    ├── 6 微信扫码
    │   ├── 6.1 接入流程
    │	│   │
    │   ├── 6.2 接入分析
    │	│   │
    │   ├── 6.3 接口定义
    │   │   ├── (1) 修改nginx.conf       
    │   │   ├── (2) 修改回调uri   
    │   │   ├── (3) RestTemplate远程调用  
    │   │   ├── (4) WxLogin  
    │   │   ├── (3) WxAuthService  
    │   │   ├── (4) excute  
    
    ├── 7 微信扫码公众号登录
    │   ├── 7.1 前端配置
    │	│   │
    │   ├── 7.2 跨域配置
    │	│   │
    │   ├── 7.3 集成微信登录sdk
    │   │   ├── (1) 引入依赖       
    │   │   ├── (2) 配置文件  
    │	│   │
    │   ├── 7.4 生成带参二维码
    │   │   ├── (1) controller       
    │   │   ├── (2) 微信登录sdk配置类 
    │	│   │
    │   ├── 7.5 事件处理器
    │   │   ├── (1) 处理器逻辑       
    │   │   ├── (2) 处理器 
    │   │   ├── (3) 配置处理器
    │	│   │
    │   ├── 7.6 服务认证准备
    │   │   ├── (1) Controller       
    │   │   ├── (2) 内网穿透 
    │   │   ├── (3) 微信公众平台 
    │   │   ├── (4) 配置callBackUrl
    │	│   │
    │   ├── 7.7 扫码发送授权链接
    │   │   ├── (1) ScanHandler      
    │   │   ├── (2) WxMsgService 
    │   │   ├── (3) Controller
    │	│   │

5 用户认证


5.1 认证流程

image20241104195840209.png

  1. 自定义了UserDetailService,重写loadUserByUsername()方法
  2. 拓展了用户信息
  3. 自定义了DaoAuthenticationProvider,重写additionalAuthenticationChecks()方法

5.2 连接数据库认证


(1) 原来的方法

  • WebSecurityConfig中的UserDetailService()方法用户信息存储在内存中

    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }
    

(2) 查询用户原理

  • UserDetailService是一个接口,其中实现了loadUserByUsername()方法

    public interface UserDetailsService {
        UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
    }
    
  • 其中UserDetails是用户信息接口

    public interface UserDetails extends Serializable {
        Collection<? extends GrantedAuthority> getAuthorities();
    
        String getPassword();
    
        String getUsername();
    
        boolean isAccountNonExpired();
    
        boolean isAccountNonLocked();
    
        boolean isCredentialsNonExpired();
    
        boolean isEnabled();
    }
    

(3) 实现数据库认证

  • 通过自定义UserDetaislService接口来实现

  • 重写loadUserByUsername()方法查询数据库得到用户信息返回UserDetails类型的用户信息即可

    /**
     * @Description: 自定义UserDetailService
     **/
    @Slf4j
    @Component
    public class UserServiceImpl implements UserDetailsService {
    
        @Autowired
        private XcUserMapper xcUserMapper;
    
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            //账号
            String username = s;
            //1.根据username账号查询数据库
            XcUser xcUser = xcUserMapper.selectOne(
                new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
    
            //2.查询到用户不存在---->返回null---->springSecurity框架会抛出异常:用户不存在
            if (xcUser == null) {
                return null;
            }
    
            //3.用户存在,拿到正确的密码,封装成UserDetails对象,返回给SpringSecurity框架,由框架进行密码比对
            String password = xcUser.getPassword(); //获取密码
            String[] authorities = {"test"};    //权限
            UserDetails userDetails =User.withUsername(username)
                                    .password(password)
                                    .authorities(authorities)
                                    .build();
            return userDetails;
        }
    }
    

(4) 修改加密方式

使用Bcrypt加密

  • WebSecurityConfig
    image20241104195840209.png

  • AuthorizationServer修改客户端密钥
    image20241104195840209.png


5.3 扩展用户信息


(1) 目前问题

  • 目前重写的UserDetailService中的loadUserByUsername()方法

    返回的UserDetail只包含了username,password等基本信息,不够用

    UserDetails userDetails =User.withUsername(username)	//这个username只是包含普通的username用户名
                                    .password(password)
                                    .authorities(authorities)
                                    .build();
    return userDetails;
    

(2) 方案一

  • 可以扩展UserDetails,使之包括更多的自定义属性(但需要修改较多代码)

(3) 方案二

  • 扩展username字段的内容,比如存入json数据作为username的内容

    此时username只是一个名字,实际上存储的是user相关信息

  • 修改重写的UserDetailService中的loadUserByUsername()方法

    //根据username账号查询数据库
    XcUser xcUser = xcUserMapper.selectOne(
        new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
    
    String password = xcUser.getPassword(); //获取密码
    String[] authorities = {"test"};    //权限
    //令牌中不存放密码(为了安全)
    xcUser.setpassword(null);
    //将数据库查询到的用户信息转为JSON
    String userString = JSON.toJSONString(xcUser);
    UserDetails userDetails =User.withUsername(userString)	//userString包含了用户的其他信息
                            .password(password)
                            .authorities(authorities)
                            .build();
    return userDetails;
    

5.4 资源服务获取用户信息


封装工具类获取用户信息

(1) 工具类

/**
 * @Description: 获取当前用户身份工具类
 **/
@Slf4j
public class SecurityUtil {

    public static XcUser getUser() {
        try {
            // 拿到当前用户信息
            Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principalObj instanceof String) {
                //取出用户身份信息
                String principal = principalObj.toString();
                //将json转成对象
                XcUser user = JSON.parseObject(principal, XcUser.class);
                return user;
            }
        } catch (Exception e) {
            log.error("获取当前登录用户身份出错:{}", e.getMessage());
            e.printStackTrace();
        }

        return null;
    }


    @Data
    public static class XcUser implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id;

        private String username;

        private String password;

        private String salt;

        private String name;
        private String nickname;
        private String wxUnionid;
        private String companyId;
        /**
         * 头像
         */
        private String userpic;

        private String utype;

        private LocalDateTime birthday;

        private String sex;

        private String email;

        private String cellphone;

        private String qq;

        /**
         * 用户状态
         */
        private String status;

        private LocalDateTime createTime;

        private LocalDateTime updateTime;
    }


}

(2) 获取用户信息

SecurityUtil.XcUser user = SecurityUtil.getUser();

5.5 统一认证入口


(1) 统一认证参数

所有认证类型通用的DTO

/**
 * @description 认证用户请求参数
 */
@Data
public class AuthParamsDto {

    private String username; //用户名
    private String password; //域  用于扩展
    private String cellphone;//手机号
    private String checkcode;//验证码
    private String checkcodekey;//验证码key
    private String authType; // 认证的类型   password:用户名密码模式类型    sms:短信模式类型
    private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId
}

(2) 修改自定义UserDetailService

修改接收的参数为AutoParamDto(统一认证参数,所有认证类型通用的DTO)
image20241104195840209.png


(3) 自定义DaoAuthenticationProvider

  1. 重写additionalAuthenticationChecks()方法,使方法内容为空

    因为原来的方法里校验了密码,但我们统一了认证入口,不是所有的认证类型都需要校验密码

  2. 重写setUserDetailsService()方法,指定我们自定义的UserDetailsService

/**
 * @Description: 自定义DaoAuthenticationProvider
 **/
@Component
@Slf4j
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {

    /** UserDetailsService已经被重新定义
     * 需要将自定义的UserDetailsService注入*/
    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService){
        //调用父类的注入方法,将自定义的UserDetailsService注入
        super.setUserDetailsService(userDetailsService);
    }


    /** 重写方法,屏蔽密码对比 */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //重写方法内容为空
    }
}

(4) 修改WebSecurityConfig

  • 因为我们重写了DaoAuthenticationProvider,

    需要告诉springSecurity框架我们自定义的DaoAuthenticationProvider是哪个

@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;

/** 告诉框架我们自定义的DaoAuthenticationProvider是哪个*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProviderCustom);
}

(5) 统一认证接口--策略模式

/**
 * @Description: 统一认证接口
 **/
public interface AuthService {
    /**
     * 认证方法
     * @param authParamsDto 认证参数
     */
    XcUserExt execute(AuthParamsDto authParamsDto);
}

image20241104195840209.png

/**
 * @Description: 微信认证
 **/
@Service("wx_authservice")		//定义beanName--------------------------
public class WxAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}
/**
 * @Description: 账号密码认证
 **/
@Service("password_authservice")	//定义beanName--------------------------
public class PasswordAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

(6) 修改自定义UserDetaillService

@Autowired
private ApplicationContext applicationContext;

@Override   //传入的参数是AuthParamsDto的json字符串
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    AuthParamsDto authParamsDto = null;
    try{
        //转成AuthParamsDto对象
        authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
    }catch (Exception e){
        log.info("认证请求不符合项目要求:{}",s);
        throw new    RuntimeException("认证请求数据格式不对");
    }

    //修改---------------------------------------------------------------------
    //获取认证类型
    String authType = authParamsDto.getAuthType();
    //根据认证类型从spring容器中取出指定的bean	(通过bean的名字取出不同认证类型对应的bean)
    String beanName = authType+"_authservice";
    AuthService bean = applicationContext.getBean(beanName, AuthService.class);
    //调用统一的excute方法		(不同认证类型对应不同的excute方法)
    XcUserExt xcUserExt = bean.execute(authParamsDto);
    //修改---------------------------------------------------------------------
    
    //封装xcUSerExt用户信息为userDetails
    UserDetails userDetails = getUserPrincipal(xcUserExt);

    return userDetails;
}

/**
 * 查询用户信息 ----------------------------------提取方法
 */
public UserDetails getUserPrincipal(XcUserExt user){
    String password = user.getPassword();
    //权限
    String[] authorities = {"test"};
    //根据用户id查询用户的权限
    List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(user.getId());

    if(xcMenus.size()>0){
        List<String> permissions= new ArrayList<>();
        xcMenus.forEach(m->{
            //拿到了用户拥有的权限标识符
            permissions.add(m.getCode());
        });
        //将permissions转成数组
        authorities = permissions.toArray(new String[0]);

    }

    //令牌中不存放密码(为了安全)
    user.setPassword(null);
    //将user对象转成json
    String userString = JSON.toJSONString(user);
    UserDetails userDetails = User.withUsername(userString)
                            .password(password)
                            .authorities(authorities)
                            .build();
    return userDetails;
}
//			  ----------------------------------提取方法

5.6 账号密码认证


PasswordAuthServiceImpl实现AuthService接口,重写execute()方法

/**
 * @Description: 账号密码认证
 **/
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {

    @Autowired
    private XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    CheckCodeClient checkCodeClient;

    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //输入的验证码
        String checkcode = authParamsDto.getCheckcode();
        //验证码的key
        String checkcodekey = authParamsDto.getCheckcodekey();

        if(StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)){
            throw new RuntimeException("请输入验证码");
        }

        //远程调用验证码服务接口---->校验验证码
        Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
        if (verify == null || !verify) {
            throw new RuntimeException("验证码输入错误");
        }

        //账号
        String username = authParamsDto.getUsername();
        //1.根据username账号查询数据库
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));

        //2.查询到用户不存在---->返回null---->springSecurity框架会抛出异常:用户不存在
        if (xcUser == null) {
            throw new RuntimeException("账号不存在");
        }

        //3.用户存在,拿到正确的密码
        String passwordDB = xcUser.getPassword(); //获取密码

        //获取用户输入的密码
        String passwordForm = authParamsDto.getPassword();

        //验证密码是否正确
        boolean matches = passwordEncoder.matches(passwordForm, passwordDB);

        if (!matches) {
            throw new RuntimeException("账号或密码错误");
        }

        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(xcUser, xcUserExt);

        return xcUserExt;
    }
}

6 微信扫码


6.1 接入流程

接口文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

image20241104195840209.png


6.2 接入分析

  1. 需要定义接口接收微信下发的授权码
  2. 收到授权码调用微信接口申请令牌
  3. 携带令牌调用微信接口获取用户信息
  4. 获取用户信息将其写入数据库
  5. 重定向到浏览器自动登录

image20241104195840209.png


6.3 接口定义


(1) 修改nginx.conf

#微信扫码回调,监听8888端口(与redirect_uri对应)
server {
    listen 8888;
    server_name localhost;

    location /api {
    proxy_pass http://gatewayserver;
    #proxy_pass http://localhost:64410;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 这里需要添加一个rewrite规则,把请求中的/api去掉
    rewrite ^/api(.*)$ $1 break;
    }
}

(2) 修改回调uri

//请用微信生成二维码
function generateWxQrcode(token) {
var wxObj = new WxLogin({
    self_redirect:true,
    id:"login_container", 
    appid: "wxed9954c01bb89b47	", 
    scope: "snsapi_login", 
    //redirect_uri: "http://tjxt-user-t.itheima.net/xuecheng/auth/wxLogin",	//修改前
    redirect_uri: "http://localhost:8888/api/auth/wxLogin",	//修改后
    state: token,
    style: "",
    href: ""
  });
}

(3) RestTemplate远程调用

使用RestTemplate调用第三方服务

//配置在启动类中
@Bean
RestTemplate restTemplate(){
    RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    return  restTemplate;
}

(4) WxLogin

用户扫码完成后,调用/auth/wxLogin接口(生成二维码时配置的redirect_uri)

/**
 * @Description: 微信扫码接口
 **/
@Slf4j
@Controller
public class WxLoginController {

    @Autowired
    WxAuthService wxAuthService;

    @RequestMapping("/wxLogin")
    public String wxLogin(String code, String state) throws IOException {
        log.debug("微信扫码回调,code:{},state:{}",code,state);
        
        //1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入本项目数据库
        XcUser xcUser = wxAuthService.wxAuth(code);	//调用==================

        if(xcUser==null){
            return "redirect:http://www.51xuecheng.cn/error.html";
        }
        String username = xcUser.getUsername();
        
        //重定向后	认证类型为wx的认证流程-->wxAuthService的excute()方法
        return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
    }
}

(5) WxAuthService

进入/wxLogin接口后

要执行 1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入数据库

  • wxAuth()方法

    @Override
    public XcUser wxAuth(String code) {
        //1.申请令牌
        Map<String, String> accessTokenMap = getAccess_token(code);
        String access_token = accessTokenMap.get("access_token");
        String openid = accessTokenMap.get("openid");
    
        //2.携带令牌查询用户信息
        Map<String, String> userinfo_map = getUserinfo(access_token, openid);
    
        //3.保存用户信息到数据库
        //注意: 将自己注入,作为代理对象,调用事务方法,事务注解才生效
        XcUser xcUser = currentProxy.addXcUser(userinfo_map);
    
        return xcUser;
    }
    
  1. getAccess_token()携带授权码申请令牌

    /**
     * 1.携带授权码申请令牌
     *  POST  "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
     *
     * 结果为JSON格式---->使用Map
     * {
     *  "access_token":"ACCESS_TOKEN",
     *  "expires_in":7200,
     *  "refresh_token":"REFRESH_TOKEN",
     *  "openid":"OPENID",
     *  "scope":"SCOPE",
     *  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
     *  }
     *
     * @param code  授权码
     * @return
     */
    private Map<String,String> getAccess_token(String code){
        String url_temple = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
        //最终的请求路径
        String url = String.format(url_temple, appid, secret, code);
    
        //远程调用url---->使用RestTemplate
        ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, null, String.class);
        //获取响应的结果
        String result = exchange.getBody();
        //将result转成map
        Map<String,String> map = JSON.parseObject(result, Map.class);
    
        return map;
    }
    
  2. getUserinfo()携带令牌查询用户信息

    /**
     * 2.携带令牌查询用户信息
     *  GET  "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"
     *
     *  响应结果为JSON---->map
     *  {
     * "openid":"OPENID",
     * "nickname":"NICKNAME",
     * "sex":1,
     * "province":"PROVINCE",
     * "city":"CITY",
     * "country":"COUNTRY",
     * "headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
     * "privilege":[
     * "PRIVILEGE1",
     * "PRIVILEGE2"
     * ],
     * "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
     * }
     *
     * @param access_token
     * @param openid
     * @return
     */
    private Map<String,String> getUserinfo(String access_token,String openid){
        String url_template= "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s";
        String url = String.format(url_template, access_token, openid);
    
        //远程调用
        ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
        //获取响应的结果---->乱码:转成UTF-8
        String result = new String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
        //将result转成map
        Map<String,String> map = JSON.parseObject(result, Map.class);
    
        return map;
    }
    
  3. addXcUser()保存用户信息到数据库

    /**
     *  3.保存用户信息到数据库
     * @param userInfo_map
     * @return
     */
    @Transactional
    public XcUser addXcUser(Map<String,String> userInfo_map){
        String unionid = userInfo_map.get("unionid");
        String nickname = userInfo_map.get("nickname");
    
        //通过unionid查询用户信息
        XcUser xcUserDB = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid));
    
        if(xcUserDB != null ){
            return xcUserDB;
        }
    
        //向数据库新增记录
        XcUser xcUser = new XcUser();
        String userId = UUID.randomUUID().toString();
        xcUser.setId(userId); //主键
        xcUser.setUsername(unionid);
        xcUser.setPassword(unionid);
        xcUser.setWxUnionid(unionid);
        xcUser.setNickname(nickname);
        xcUser.setName(nickname);
        xcUser.setUtype("101001");  //学生类型
        xcUser.setStatus("1");  //用户状态
        xcUser.setCreateTime(LocalDateTime.now());
        //插入
        xcUserMapper.insert(xcUser);
    
    
        //向用户角色关系表新增记录
        XcUserRole xcUserRole = new XcUserRole();
        xcUserRole.setId(UUID.randomUUID().toString());
        xcUserRole.setUserId(userId);
        xcUserRole.setRoleId("17"); //学生角色
        xcUserRole.setCreateTime(LocalDateTime.now());
        xcUserRoleMapper.insert(xcUserRole);
    
        return xcUser;
    }
    

(6) excute

扫码后调用/wxLogin接口,执行完 (1.远程微信申请令牌,2.拿到令牌查询用户信息,3.将用户信息写入本项目数据库) 后

重定向http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx

(此处的username实际上是统一认证参数AuthParamDto的json串)

(执行认证类型为wx的认证流程-->wxAuthServiceexcute()方法)

@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {

    //得到账号
    String username = authParamsDto.getUsername();

    //查询数据库
    XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
    if(xcUser == null){
        throw new RuntimeException("用户不存在");
    }

    XcUserExt xcUserExt = new XcUserExt();
    BeanUtils.copyProperties(xcUser,xcUserExt);

    return xcUserExt;
}

7 微信扫码公众号登录


7.1 前端配置


<!DOCTYPE html>
<html lang="zh-CN">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>微信登录二维码</title>
    </head>

    <body>
        <div class="container">
            <img src="../img/wx-icon.png" alt="微信登录" class="wechat-icon" onclick="generateQRCode()">
            <div id="qrCodeContainer">
                <h3>扫描二维码登录</h3>
                <img id="qrCode" alt="二维码">
            </div>
        </div>

        <script>
            function generateQRCode() {
                // 获取二维码容器
                const qrCodeContainer = document.getElementById("qrCodeContainer");
                const qrCodeImg = document.getElementById("qrCode");

                // 显示二维码容器
                qrCodeContainer.style.display = "block";

                // 通过后端接口获取二维码
                fetch("http://127.0.0.1:8099/wx/public/gen", {
                    method: "GET",  // 请求方式
                    credentials: "include"  // 允许携带凭证(如 Cookies 或认证信息)
                })
                    .then(response => response.text())  // 由于返回的是URL字符串,所以使用 text() 解析
                    .then(url => {
                        // 假设API返回的JSON数据中二维码的URL在data.qrCodeUrl中
                        if (url) {
                            qrCodeImg.src = url;
                        } else {
                            qrCodeImg.alt = "二维码数据不正确";
                        }
                    })
                    .catch(error => {
                        console.error("获取二维码失败:", error);
                        qrCodeImg.alt = "二维码加载失败";
                    });
            }

        </script>
    </body>

</html>
<style>
    /* 简单的页面样式 */
    .container {
        text-align: center;
        margin-top: 50px;
    }

    /* 微信登录图标按钮 */
    .wechat-icon {
        width: 60px;
        height: 60px;
        cursor: pointer;
        border-radius: 5px;
        /* 圆形按钮 */
        border: 2px solid #25d366;
        /* 微信绿色 */
        padding: 10px;
        background-color: #f5f5f5;
        transition: all 0.3s ease;
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
        /* 阴影 */
    }

    /* 鼠标悬停效果 */
    .wechat-icon:hover {
        transform: scale(1.1);
        /* 放大效果 */
        background-color: #e0e0e0;
        /* 变为微信绿色背景 */
        box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.3);
        /* 增强阴影 */
    }

    #qrCodeContainer {
        margin-top: 20px;
        display: none;
        /* 初始隐藏 */
    }

    #qrCode {
        width: 200px;
        height: 200px;
        border: 1px solid #ddd;
    }
</style>

7.2 跨域配置


/**
 * @Author: ciky
 * @Description: webMVC配置类
 * @DateTime: 2024/11/13 11:48
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {

    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/**")  // 允许所有路径
                .allowedOrigins("http://127.0.0.1:5500")  // 允许 localhost:5500 域名的请求 
            	.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方式
                .allowCredentials(true) // 允许携带认证信息(如 Cookie)
                .maxAge(3600)
                .allowedHeaders("*");    // 允许所有请求头
    }
}

7.3 集成微信登录sdk


(1) 引入依赖

<!--微信登录sdk-->
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.4.0</version>
</dependency>

(2) 配置文件

wx:
  mp:
    callback: ${mallchat.wx.callback}
    configs:
      - appId: ${mallchat.wx.appId} # 第一个公众号的appid
        secret: ${mallchat.wx.secret} # 公众号的appsecret
        token: ${mallchat.wx.token} # 接口配置里的Token值
        aesKey: ${mallchat.wx.aesKey} # 接口配置里的EncodingAESKey值(测试号不需要)

image20241104195840209.png


7.4 生成带参二维码


(1) Controller

/**
 * @Author: ciky
 * @Description: 微信登录api交互接口
 * @DateTime: 2024/11/13 11:26
 **/
@RestController
@RequestMapping("/wx/public")
public class WxLoginController {
    private static final Logger LOG = LoggerFactory.getLogger(WxLoginController.class);

    @Autowired
    private WxMpService wxMpService;

    @GetMapping("/gen") 
    public String getQrCode() throws WxErrorException {
        // 获取临时二维码ticket
        WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(1, 10000);

        // 用ticket获取二维码图片url地址
        String url = wxMpService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
        LOG.info("申请的二维码:{}",url);
        return url;
    }
}

(2) 微信登录sdk配置类

/**
 * @Author: ciky
 * @Description: 微信登录sdk配置类
 * @DateTime: 2024/11/13 11:33
 **/
@Configuration
public class WxMpConfig {

    @Value("${wx.mp.appId}")
    private String appId;

    @Value("${wx.mp.secret}")
    private String secret;

    @Value("${wx.mp.token}")
    private String token;

    @Value("${wx.mp.aesKey}")
    private String aesKey;

    @Bean
    public WxMpService wxMpService() {

        //创建一个WxMpServiceImpl实例(该类实现了 WxMpService 接口,负责与微信公众平台的交互)
        WxMpService service = new WxMpServiceImpl();
        WxMpDefaultConfigImpl configStore = new WxMpDefaultConfigImpl();

        configStore.setAppId(appId);
        configStore.setSecret(secret);
        configStore.setToken(token);
        configStore.setAesKey(aesKey);

        service.setWxMpConfigStorage(configStore);

        return service;
    }
}

7.5 事件处理器


(1) 处理器逻辑

  1. AbstractHandler实现WxMpMessageHandler接口,
  2. ScanHandler继承AbstractHandler
  3. SubscribeHandler继承AbstractHandler

(2) 处理器

/**
 * @Description: 处理器抽象类
 **/
public abstract class AbstractHandler implements WxMpMessageHandler {
}
/**
 * @Description: 扫码事件处理器
 **/
@Component
public class ScanHandler extends AbstractHandler {
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
        return null;
    }
}
/**
 * @Description: 新关注事件处理器
 **/
@Component
public class SubscribeHandler extends AbstractHandler{
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
        return null;
    }
}

(3) 配置处理器

/**
 * @Description: 微信登录sdk配置类
 **/
@Configuration
public class WxMpConfig {

    @Autowired
    private ScanHandler scanHandler;  //扫码事件处理器

    @Autowired
    private SubscribeHandler subscribeHandler;  //扫码事件处理器

    @Bean
    public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
        final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);

        // 关注事件
        newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();

        // 扫码事件
        newRouter.rule().async(false).msgType(EVENT).event(WxConsts.EventType.SCAN).handler(this.scanHandler).end();

        return newRouter;
    }
}

7.6 服务认证准备


(1) Controller

/**
 * @Author: ciky
 * @Description: 微信登录api交互接口
 * @DateTime: 2024/11/13 11:26
 **/
@RestController
@RequestMapping("/wx/public")
public class WxLoginController {
    private static final Logger LOG = LoggerFactory.getLogger(WxLoginController.class);

    @Autowired
    private WxMpService wxMpService;

    @Autowired
    private WxMpMessageRouter wxMpMessageRouter;

    @GetMapping(produces = "text/plain;charset=utf-8")
    public String authGet(@RequestParam(name = "signature", required = false) String signature,
                          @RequestParam(name = "timestamp", required = false) String timestamp,
                          @RequestParam(name = "nonce", required = false) String nonce,
                          @RequestParam(name = "echostr", required = false) String echostr) {

        LOG.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
                timestamp, nonce, echostr);
        if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
            throw new IllegalArgumentException("请求参数非法,请核实!");
        }


        if (wxMpService.checkSignature(timestamp, nonce, signature)) {
            return echostr;
        }

        return "非法请求";
    }

    @PostMapping(produces = "application/xml; charset=UTF-8")
    public String post(@RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam("openid") String openid,
                       @RequestParam(name = "encrypt_type", required = false) String encType,
                       @RequestParam(name = "msg_signature", required = false) String msgSignature) {
        LOG.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
                        + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
                openid, signature, encType, msgSignature, timestamp, nonce, requestBody);

        if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
            throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
        }

        String out = null;
        if (encType == null) {
            // 明文传输的消息
            WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
            WxMpXmlOutMessage outMessage = this.route(inMessage);
            if (outMessage == null) {
                return "";
            }

            out = outMessage.toXml();
        } else if ("aes".equalsIgnoreCase(encType)) {
            // aes加密的消息
            WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(),
                    timestamp, nonce, msgSignature);
            LOG.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
            WxMpXmlOutMessage outMessage = this.route(inMessage);
            if (outMessage == null) {
                return "";
            }

            out = outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
        }

        LOG.debug("\n组装回复信息:{}", out);
        return out;
    }

    private WxMpXmlOutMessage route(WxMpXmlMessage message) {
        try {
            return this.wxMpMessageRouter.route(message);
        } catch (Exception e) {
            LOG.error("路由消息时出现异常!", e);
        }

        return null;
    }
}

(2) 内网穿透

cpolar http 8099

image20241104195840209.png


(3) 微信公众平台

微信公众平台

image20241104195840209.png
image-20240907120023832

(4) 配置callBackUrl

wx:
  mp:
    callback: http://42d6c9c2.r22.cpolar.top

7.7 扫码发送授权链接


(1) ScanHandler

扫码后监听到扫码事件,进行处理

/**
 * @Description: 扫码事件处理器
 **/
@Component
public class ScanHandler extends AbstractHandler {

    @Autowired
    @Lazy
    private WxMsgService wxMsgService;

    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
        return wxMsgService.scan(wxMpXmlMessage);
    }
}

(2) WxMsgService

扫码事件的处理逻辑

image20241104195840209.png

  • 授权接口:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
  • 回调地址:http://42d6c9c2.r22.cpolar.top/wx/public/callBack (自定义)
/**
 * @Description: 微信事件处理逻辑实现类
 **/
@Service
public class WxMsgServiceImpl implements WxMsgService {
    private static final Logger LOG = LoggerFactory.getLogger(WxMsgServiceImpl.class);

    @Value("${wx.mp.callback}")
    private String callback;

    @Autowired
    private WxMpService wxMpService;

    /**
     *  授权接口url
     */
    public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";

    public WxMpXmlOutMessage scan(WxMpXmlMessage wxMpXmlMessage) {
        LOG.info("---扫码事件开始---");
        // 获取openId
        String openId = wxMpXmlMessage.getFromUser();
        // 获取授权码code
        Integer code = getEventKey(wxMpXmlMessage);
        LOG.info("openId:{},code:{}",openId,code);

        if(Objects.isNull(code)){
            return null;
        }

        /**
         * 授权接口:
         * https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
         */
        String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(callback+"/wx/public/callBack"));
        // 向用户返回授权链接
        return WxMpXmlOutMessage.TEXT().content("请点击链接授权:<a href=\""+authorizeUrl+"\"> 登录 </a>")
                .fromUser(wxMpXmlMessage.getToUser())
                .toUser(wxMpXmlMessage.getFromUser())
                .build();
    }

    /**
     * 获取授权码code
     */
    private Integer getEventKey(WxMpXmlMessage wxMpXmlMessage) {
        try {
            //事件码:qrscene_2 --> 新关注
            String eventKey = wxMpXmlMessage.getEventKey();
            String code = eventKey.replace("qrscene_", "");
            return Integer.parseInt(code);
        }catch (Exception e){
            LOG.error("getEventKey error, eventKey:{}",wxMpXmlMessage.getEventKey());
            return null;
        }
    }	
}

(3) Controller

@GetMapping("/callBack")
public RedirectView callBack(@RequestParam String code) throws WxErrorException {
    /**
     * 用户点击公众号发的"授权"连接后,回调此方法
     * 1. 通过code换取access_token
     * 2. 通过access_token获取用户信息
     */
    WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
    WxOAuth2UserInfo userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken, "zh_cCN");
    LOG.info("获取到的用户信息:{}",userInfo);

    RedirectView redirectView = new RedirectView();
    redirectView.setUrl("https://www.ciky.cloud");
    return redirectView;
}
获取到的用户信息:
WxOAuth2UserInfo(openid=oOQUs6ghkB4RMZes4JfdgW3D1PR8, nickname=Ciky, sex=0, city=, province=, country=, headImgUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/PiajxSqBRaELbLvJlx2ibZZHKIHZS7e169ibL1W8EeueM8LZ4dpAtMgC6UCNHVW9Mf0ZM4Tgw9tRbDH0TNmezVKfrl4zdrhGoBic1oYVz2XxmxvYPIaugDd7ww/132, 
unionId=null, privileges=[], snapshotUser=null)