JavaでWeChatスキャンコードログインを実現し、認証・認可を実現

JavaでWeChatスキャンコードログインを実現し、認証・認可を実現

1. ログインプロセスと原則

1.1 OAuth2プロトコル

Web サイト アプリケーション WeChat ログインは、OAuth2.0 プロトコル標準に基づいた WeChat OAuth2.0 認定ログイン システムです。WeChat OAuth2.0認証ログインアクセスを実行する前に、WeChatオープンプラットフォームに開発者アカウントを登録し、承認されたWebサイトアプリケーションを持ち、対応するAppIDとAppSecretを取得してください。プロセスへの受信を開始します。

プログラムの流れ:

	 +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

OAuth2 には次の役割が含まれます。

1.クライアント

リソース自体は保存されず、モバイル クライアント、ブラウザなどのリソース所有者の承認を通じてリソース サーバーのリソースを要求する必要があります。

2. リソース所有者

通常はユーザーですが、アプリケーション、つまりリソー​​スの所有者でもあります。

A は、クライアントがリソース所有者に承認を要求していることを示します。

B は、リソース所有者がクライアントに自身のユーザー情報へのアクセスを許可していることを示します。

3. 認可サーバー (認証サーバーとも呼ばれます)

認証サーバーはリソース所有者を認証するとともに、クライアントを認証してトークンを発行します。

C クライアントは、認証を要求するための許可コードを運びます。

D はトークンを発行して認証します。

4. リソースサーバー

リソースを保存するサーバー。

E は、クライアントがトークンを保持してリソース サーバーにリソースの取得を要求することを意味します。

F は、検証トークンが渡された後にリソース サーバーが保護されたリソースを提供することを示します。

2.2 WeChat スキャンコードのログインプロセス

例として、ブラウザーでのスキャン コード ログインを取り上げます。

WeChatスキャンコードログインプロセス

認証ログインプロセス:

1. ユーザーはウェブサイトへのログインを申請し、WeChat QR コードをスキャンし、WeChat にログインの承認を要求します。

2. ユーザーが確認すると、WeChat 端末には Web サイトにリダイレクトするコードが送信されます。

3. Web サイトは、WeChat からの access_token を申請するためのコード、appid、および appsecret を提供します。

4. WeChat は access_token を返し、Web サイトは access_token を取得して WeChat サーバーからユーザー情報を取得します。

5. Web サイトが情報を取得すると、ログイン インターフェイスにリダイレクトされ、ログインが成功します。

2. コードの実装

このプロジェクト認証サービスを利用するには何をする必要がありますか?

1. WeChatが発行する認証コードを受け取るインターフェースを定義する必要があります。

2. 認証コードを受信したら、WeChat インターフェイスを呼び出してトークンを申請します。

3. トークンを申請し、WeChat に電話してユーザー情報を取得します

4. ユーザー情報を取得し、このプロジェクトのユーザー センター データベースに正常に書き込みます。

5. 最後に、ブラウザにリダイレクトして自動的にログインします。

コードは以下のように表示されます。

2.1 コントローラー

@Controller
public class WxLoginController {
    
    

 @Autowired
 WxAuthServiceImpl wxAuthService;

 /**
  * 用户扫码确认登录后进入该接口,收到wx端重定向传过来的授权码,用授权码申请令牌,查询用户信息,写入用户信息
  * @param code 微信端返回的授权码
  * @param state 用于保持请求和回调的状态,授权请求后原样带回给第三方。
  *              该参数可用于防止 csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,
  *              可设置为简单的随机数加 session 进行校验
  * @return
  * @throws IOException
  */
  @RequestMapping("/wxLogin")
  public String wxLogin(String code, String state) throws IOException {
    
    

     //拿授权码申请令牌,查询用户
   XcUser xcUser = wxAuthService.wxAuth(code);
   if(xcUser == null){
    
    
     //重定向到一个错误页面
     return "redirect:http://www.xxxxxxx.com/error.html";
   }else{
    
    
    String username = xcUser.getUsername();
    //重定向到登录页面,自动登录
     return "redirect:http://www.xxxxxxx.com/sign.html?username="+username+"&authType=wx";
   }
  }
}

2.2 WxAuthServiceImpl

ここではサービスを使用してクラスを直接実装します。

@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService {
    
    

    @Autowired
    UserMapper userMapper;
    @Value("${weixin.appid}")
    String appid;
    @Value("${weixin.secret}")
    String secret;
    @Autowired
    RestTemplate restTemplate;
    @Autowired
    UserRoleMapper userRoleMapper;
    @Autowired
    WxAuthServiceImpl currentProxy;

    //拿授权码申请令牌,查询用户
    public User wxAuth(String code) {
    
    
        //拿授权码获取access_token
        Map<String, String> access_token_map = getAccess_token(code);
        System.out.println(access_token_map);
        //得到令牌
        String access_token = access_token_map.get("access_token");
        //得到openid
        String openid = access_token_map.get("openid");
        //拿令牌获取用户信息
        Map<String, String> userinfo = getUserinfo(access_token, openid);
        System.out.println(userinfo);
        //添加用户到数据库
        User User = currentProxy.addWxUser(userinfo);

        return User;
    }

    @Transactional
    public User addWxUser(Map userInfo_map){
    
    

        //先取出unionid
        String unionid = (String) userInfo_map.get("unionid");
        //根据unionid查询数据库
        User User = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getWxUnionid, unionid));
        if(User!=null){
    
    
            //该用户在系统存在
            return User;
        }
        User = new User();
        //用户id
        String id = UUID.randomUUID().toString();
        User.setId(id);
        User.setWxUnionid(unionid);
        //记录从微信得到的昵称
        User.setNickname(userInfo_map.get("nickname").toString());
        User.setUserpic(userInfo_map.get("headimgurl").toString());
        User.setName(userInfo_map.get("nickname").toString());
        User.setUsername(unionid);
        User.setPassword(unionid);
        User.setUtype("101001");//学生类型
        User.setStatus("1");//用户状态
        User.setCreateTime(LocalDateTime.now());
        userMapper.insert(User);
        UserRole UserRole = new UserRole();
        UserRole.setId(UUID.randomUUID().toString());
        UserRole.setUserId(id);
        UserRole.setRoleId("17");//学生角色
        userRoleMapper.insert(UserRole);
        return User;

    }

    //请求微信获取令牌

    /**
     * 微信接口响应结果
     * {
     * "access_token":"ACCESS_TOKEN",
     * "expires_in":7200,
     * "refresh_token":"REFRESH_TOKEN",
     * "openid":"OPENID",
     * "scope":"SCOPE",
     * "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
     * }
     */
    private Map<String, String> getAccess_token(String code) {
    
    
        String url_template = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
        String url = String.format(url_template, appid, secret, code);
        //请求微信获取令牌
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, null, String.class);

        System.out.println(response);
        //得到响应串
        String responseString = response.getBody();
        //将json串转成map
        Map map = JSON.parseObject(responseString, Map.class);
        return map;
    }

    //携带令牌查询用户信息
    //http请求方式: GET
    //https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
    /**
     {
     "openid":"OPENID",
     "nickname":"NICKNAME",
     "sex":1,
     "province":"PROVINCE",
     "city":"CITY",
     "country":"COUNTRY",
     "headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJfHe/0",
     "privilege":[
     "PRIVILEGE1",
     "PRIVILEGE2"
     ],
     "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"

     }
    */
    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> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
        String body = response.getBody();
        //将结果转成map
        Map map = JSON.parseObject(body, Map.class);
        return map;

    }
}

3. 認証と認可

プロジェクトには Spring Security が統合されており、ユーザー情報からユーザーの権限情報を取得する必要があります。

3.1 UserServiceImpl

Spring Security のユーザー認証メソッドを書き換えて、WeChat ログイン認証へのアクセスを可能にしました。

authParamsD 認証パラメータ定義へ。

/**
 * @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
}

ユーザー拡張情報の定義。

/**
 * @description 用户扩展信息
 */
@Data
public class XcUserExt extends XcUser {
    
    
    //用户权限
    List<String> permissions = new ArrayList<>();
}

loadUserByUsername() メソッドは、WeChat 認証をサポートするように書き直されました。

@Slf4j
@Service
public class UserServiceImpl implements UserDetailsService {
    
    

    @Autowired
    UserMapper userMapper;
    @Autowired
    ApplicationContext applicationContext;
    @Autowired
    MenuMapper menuMapper;//菜单权限mapper

    //传入的是AuthParamsDto的json串
    @Override
    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实例
        AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class);
        //开始认证,认证成功拿到用户信息
        UserExt UserExt = authService.execute(authParamsDto);

        return getUserPrincipal(UserExt);
    }
    //根据UserExt对象构造一个UserDetails对象
    /**
     * @description 查询用户信息
     * @param user  用户id,主键
     * @return 用户信息
     */
    public UserDetails getUserPrincipal(UserExt user){
    
    

        //权限列表,存放的用户权限
        List<String> permissionList = new ArrayList<>();

        //根据用户id查询数据库中他的权限
        List<Menu> Menus = menuMapper.selectPermissionByUserId(user.getId());
        Menus.forEach(menu->{
    
    
            permissionList.add(menu.getCode());
        });
        if(permissionList.size()==0){
    
    
            //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
            permissionList.add("test");
        }

        String[] authorities= permissionList.toArray(new String[0]);
        //原来存的是账号,现在扩展为用户的全部信息(密码不要放)
        user.setPassword(null);
        String jsonString = JSON.toJSONString(user);
        UserDetails userDetails = User.withUsername(jsonString).password("").authorities(authorities).build();

        return userDetails;
    }

}

3.2 サービスインターフェース

public interface AuthService {
    
    

  /**
   * @description 认证方法
   * @param authParamsDto 认证参数
   * @return 用户信息
   */
  UserExt execute(AuthParamsDto authParamsDto);

}

上記のメソッドはexecute()WeChatログインサービス実装クラスにWxAuthServiceImpl実装されています。

//微信认证方法
    @Override
    public UserExt execute(AuthParamsDto authParamsDto) {
    
    
        //获取账号
        String username = authParamsDto.getUsername();
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername,username));
        if(user==null){
    
    
            throw new RuntimeException("用户不存在");
        }
        UserExt userExt = new UserExt();
        BeanUtils.copyProperties(user, userExt);

        return userExt;
    }

3.3 DaoAuthenticationProvider をカスタマイズする;

Spring Security フレームワークはデフォルトでパスワード検証モードになっており、パスワードを検証しないように空に書き換えられます。

@Slf4j
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
    
    

 @Autowired
 @Override
 public void setUserDetailsService(UserDetailsService userDetailsService) {
    
    
  super.setUserDetailsService(userDetailsService);
 }

 @Override
 //不再校验密码
 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    

 }

クラスを変更してWebSecurityConfigカスタムを指定しますdaoAuthenticationProviderCustom

@Autowired
    DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;

    //使用自己定义DaoAuthenticationProviderCustom来代替框架的DaoAuthenticationProvider
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.authenticationProvider(daoAuthenticationProviderCustom);
    }

これまでのところ、Spring Security に基づいて認証プロセスを次のように変更しました。

画像-20230221184043661

終了!

おすすめ

転載: blog.csdn.net/dedede001/article/details/129147893