日期:2023-06-13 16:23:13 来源:博客园
https://open.weixin.qq.com
(1)注册开发者账号:准备营业执照
(资料图片)
(2)邮箱激活
(3)完善开发者资料
(4)开发者资质认证:1-2个工作日审批、300元
(5)创建网站应用:提交审核,7个工作日审批(免费)
(6)熟悉微信登录流程
参考文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
OAuth 2.0
是目前最流行的授权机制,用来授权第三方应用,获取用户数据。
OAuth 2.0 一词中的 “Auth” 表示 “授权”,字母 “O” 是 Open 的简称,表示 “开放” ,连在一起就表示 “开放授权”。这也是为什么我们使用 OAuth 的场景,通常发生在开放平台的环境下。
越来越多的第三方应用都在向用户提供使用微信登录的解决方案,来减少用户注册的繁琐操作。而这个解决方案的背后原理,也是我们要讲到的 OAuth 2.0 技术。
我住在一个大型的居民小区。
小区有门禁系统。
进入的时候需要输入密码。
我经常网购和外卖,每天都有快递员来送货。我必须找到一个办法,让快递员通过门禁系统,进入小区。
如果我把自己的密码,告诉快递员,他就拥有了与我同样的权限,这样好像不太合适。万一我想取消他进入小区的权力,也很麻烦,我自己的密码也得跟着改了,还得通知其他的快递员。
有没有一种办法,让快递员能够自由进入小区,又不必知道小区居民的密码,而且他的唯一权限就是送货,其他需要密码的场合,他都没有权限?
于是,我设计了一套授权机制。
第一步,门禁系统的密码输入器下面,增加一个按钮,叫做"获取授权"。快递员需要首先按这个按钮,去申请授权。
第二步,他按下按钮以后,屋主(也就是我)的手机就会跳出对话框:有人正在要求授权。系统还会显示该快递员的姓名、工号和所属的快递公司。
我确认请求属实,就点击按钮,告诉门禁系统,我同意给予他进入小区的授权。
第三步,门禁系统得到我的确认以后,向快递员显示一个进入小区的令牌(access token)。令牌就是类似密码的一串数字,只在短期内(比如七天)有效。
第四步,快递员向门禁系统输入令牌,进入小区。
有人可能会问,为什么不是远程为快递员开门,而要为他单独生成一个令牌?这是因为快递员可能每天都会来送货,第二天他还可以复用这个令牌。另外,有的小区有多重门禁,快递员可以使用同一个令牌通过它们。
我们把上面的例子搬到互联网,就是 OAuth
的设计了。
首先,居民小区就是储存用户数据的网络服务。比如,微信储存了我的好友信息,获取这些信息,就必须经过微信的"门禁系统"。
其次,快递员(或者说快递公司)就是第三方应用
,想要穿过门禁系统,进入小区。
最后,我就是用户本人,同意授权第三方应用进入小区,获取我的数据。
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。这也是为什么令牌的有效期,一般都设置得很短的原因。
OAuth 的核心就是向第三方应用颁发令牌,OAuth 2.0 的标准是 RFC 6749 文件。由于互联网有多种场景,标准中定义了获得令牌的四种授权方式(authorization grant ):
注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(AppID)
和客户端密钥(AppSecret)
。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
参考文档:https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
微信登录使用了授权码方式
资料:资料>微信扫码登录>guigu_syt_user.sql
在service-user中添加依赖:
com.atguigu model 1.0 com.atguigu service-util 1.0 com.atguigu spring-security 1.0 mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test
找到service-util模块中的代码生成器,修改moduleName为user
,并执行,然后删除entity包,相关类中引入model模块中的类
在server-user模块中resources目录下创建文件
application.yml
:
spring: application: name: service-user profiles: active: dev,redis
application-dev.yml
:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:com/atguigu/syt/user/mapper/xml/*.xmlserver: port: 8203spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver password: 123456 url: jdbc:mysql://localhost:3306/guigu_syt_user?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false username: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 logging: level: root: info file: path: user feign: client: config: default: connect-timeout: 2000 #连接建立的超时时长,单位是ms,默认1s read-timeout: 2000 #处理请求的超时时间,单位是ms,默认为1s sentinel: enabled: true #开启Feign对Sentinel的支持 wx: open: app-id: wxc606fb748aedee7c # 微信开放平台 appid app-secret: 073e8e1117c1054b14586c8aa922bc9c #微信开放平台 appsecret redirect-uri: http://localhost:8200/api/user/wx/callback #微信开放平台 重定向url syt-base-url: http://localhost:3000 #预约挂号平台baserul
注意:此处重定向url的主机地址必须为 localhost:8200,因为这是在微信开放平台中预先配置的参数。生产环境中这个参数需要根据实际情况进行修改。
package com.atguigu.syt.user;@SpringBootApplication@ComponentScan(basePackages = {"com.atguigu"})public class ServiceUserApplication { public static void main(String[] args) { SpringApplication.run(ServiceUserApplication.class, args); }}
创建utils包,创建ConstantProperties.java常量类
package com.atguigu.syt.user.utils;@Configuration@ConfigurationProperties(prefix="wx.open") //读取节点@Data //使用set方法将wx.ope节点中的值填充到当前类的属性中public class ConstantProperties { private String appId; private String appSecret; private String redirectUri; private String sytBaseUrl;}
避免红色提示:
在service的pom.xml中添加如下依赖
org.springframework.boot spring-boot-configuration-processor true
service-user微服务中创建controller.front包,front包中创建FrontWxController
package com.atguigu.syt.user.controller.front;@Api(tags = "微信扫码登录")@Controller//注意这里没有配置 @RestController@RequestMapping("/front/user/wx")@Slf4jpublic class FrontWxController { @Resource private ConstantProperties constantProperties; @GetMapping("login") public String login(HttpSession session){ try { StringBuffer baseUrl = new StringBuffer() .append("https://open.weixin.qq.com/connect/qrconnect") .append("?appid=%s") .append("&redirect_uri=%s") .append("&response_type=code") .append("&scope=snsapi_login") .append("&state=%s") .append("#wechat_redirect"); //处理回调url String redirectUri = URLEncoder.encode(constantProperties.getRedirectUri(), "UTF-8"); //处理state:生成随机数,存入session //ThreadLocalRandom解决了Random在高并发环境下随机数生成性能问题 long nonce = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); //十六进制表示的随机数 String state = Long.toHexString(nonce); log.info("生成 state = " + state); session.setAttribute("wx_open_state", state); String qrcodeUrl = String.format( baseUrl.toString(), constantProperties.getAppId(), redirectUri, state ); return "redirect:" + qrcodeUrl; } catch (Exception e) { throw new GuiguException(ResultCodeEnum.URL_ENCODE_ERROR, e); } }}
在server-gateway中添加如下配置
- id: service-user predicates: Path=/*/user/** uri: lb://service-user
参考资料:https://baike.baidu.com/item/跨站请求伪造/13777878?fr=aladdin
跨站请求伪造,Cross-site request forgery,通常缩写为 CSRF或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
如果有账户名为Tom的用户访问了恶意站点,而他之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
添加校验token
由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是请求中的一个数据项。服务器将其生成并附加在请求中,其内容是一个伪随机数。当客户端提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
注意:微信服务器配置授权回调域
要和redirect-uri
一致
service-user微服务中创建controller.api包,api包中创建ApiWxController
package com.atguigu.syt.user.controller.api;@Api(tags = "微信扫码登录回调")@Controller//注意这里没有配置 @RestController@RequestMapping("/api/user/wx")@Slf4jpublic class ApiWxController { /** * 登录回调 * @param code * @param state * @param session * @return */ @GetMapping("callback") public String callback(String code, String state, HttpSession session) { //得到授权临时票据code和state参数 log.info("callback被调用"); log.info("code = " + code); log.info("state = " + state); String sessionState = (String) session.getAttribute("wx_open_state"); log.info("sessionState = " + sessionState); log.info("seesion_id = " + session.getId()); if (StringUtils.isEmpty(code) || StringUtils.isEmpty(state) || !state.equals(sessionState)) { throw new GuiguException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR); } return null; }}
根据微信的openid判断数据库是否存在当前用户信息
接口:UserInfoService
/** * 根据openid查询用户信息 * @param openid * @return */UserInfo getByOpenId(String openid);
实现:UserInfoServiceImpl
@Overridepublic UserInfo getByOpenId(String openid) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserInfo::getOpenid, openid); return baseMapper.selectOne(queryWrapper);}
资料:资料>微信扫码登录>CookieUtils.java
放入service-util模块
package com.atguigu.syt.user.controller.api;@Api(tags = "微信扫码登录回调")@Controller//注意这里没有配置 @RestController@RequestMapping("/api/user/wx")@Slf4jpublic class ApiWxController { @Resource private ConstantProperties constantProperties; @Resource private UserInfoService userInfoService; @Resource private RedisTemplate redisTemplate; /** * 登录回调 * @param code * @param state * @param session * @return */ @GetMapping("callback") public String callback(String code, String state, HttpSession session, HttpServletResponse response) { try { //得到授权临时票据code和state参数 log.info("callback被调用"); log.info("code = " + code); log.info("state = " + state); String sessionState = (String) session.getAttribute("wx_open_state"); log.info("sessionState = " + sessionState); log.info("seesion_id = " + session.getId()); if (StringUtils.isEmpty(code) || StringUtils.isEmpty(state) || !state.equals(sessionState)) { throw new GuiguException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR); } //使用code和appid以及appscrect换取access_token StringBuffer baseAccessTokenUrl = new StringBuffer() .append("https://api.weixin.qq.com/sns/oauth2/access_token") .append("?appid=%s") .append("&secret=%s") .append("&code=%s") .append("&grant_type=authorization_code"); String accessTokenUrl = String.format(baseAccessTokenUrl.toString(), constantProperties.getAppId(), constantProperties.getAppSecret(), code); //使用httpclient发送请求 byte[] respdata = HttpUtil.doGet(accessTokenUrl); String result = new String(respdata); log.info("accesstokenInfo:" + result); JSONObject resultJson = JSONObject.parseObject(result); if (resultJson.getString("errcode") != null) { log.error("获取access_token失败:" + resultJson.getString("errcode") + resultJson.getString("errmsg")); throw new GuiguException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD); } String accessToken = resultJson.getString("access_token"); String openId = resultJson.getString("openid"); log.info(accessToken); log.info(openId); //根据access_token获取微信用户的基本信息 //先根据openid进行数据库查询 UserInfo userInfo = userInfoService.getByOpenId(openId); if (userInfo != null) { //存在 log.info("判断用户是否被禁用"); if(userInfo.getStatus() == UserStatusEnum.LOCK.getStatus()){ log.error("用户已被禁用"); throw new GuiguException(ResultCodeEnum.LOGIN_DISABLED_ERROR); } }else{ log.info("注册用户"); //使用access_token换取受保护的资源:微信的个人信息 String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=%s" + "&openid=%s"; //使用httpclient发送请求 String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openId); byte[] respdataUser = HttpUtil.doGet(userInfoUrl); String resultUserInfo = new String(respdataUser); JSONObject resultUserInfoJson = JSONObject.parseObject(resultUserInfo); if (resultUserInfoJson.getString("errcode") != null) { log.error("获取用户信息失败:" + resultUserInfoJson.getString("errcode") + resultUserInfoJson.getString("errmsg")); throw new GuiguException(ResultCodeEnum.FETCH_USERINFO_ERROR); } //解析用户信息 String nickname = resultUserInfoJson.getString("nickname"); String headimgurl = resultUserInfoJson.getString("headimgurl"); //用户注册 userInfo = new UserInfo(); userInfo.setOpenid(openId); userInfo.setNickName(nickname); userInfo.setHeadImgUrl(headimgurl); userInfo.setStatus(UserStatusEnum.NORMAL.getStatus()); userInfoService.save(userInfo); } //获取用户名,如果没有用户名(未实名认证),则获取昵称 String name = userInfo.getName(); if (StringUtils.isEmpty(name)) { name = userInfo.getNickName(); } //生成token String token = UUID.randomUUID().toString().replaceAll("-", ""); //将token做key,用户id做值存入redis redisTemplate.opsForValue()//30分钟 .set("user:token:" + token, userInfo.getId(), 30, TimeUnit.MINUTES); //将token和name存入cookie //将"资料>微信登录>CookieUtils.java"放入service-utils模块 int cookieMaxTime = 60 * 30;//30分钟 CookieUtils.setCookie(response, "token", token, cookieMaxTime); CookieUtils.setCookie(response, "name", URLEncoder.encode(name), cookieMaxTime); CookieUtils.setCookie(response, "headimgurl", URLEncoder.encode(userInfo.getHeadImgUrl()), cookieMaxTime); return "redirect:" + constantProperties.getSytBaseUrl(); } catch (GuiguException e) { log.error(ExceptionUtils.getStackTrace(e)); return "redirect:" + constantProperties.getSytBaseUrl() + "?code=201&message=" + URLEncoder.encode(e.getMsg()); } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); return "redirect:" + constantProperties.getSytBaseUrl() + "?code=201&message="+URLEncoder.encode("登录失败"); } }}
资料:资料>微信扫码登录>myheader.vue
将myheader.vue复制到前端项目的layouts目录中,覆盖原来的文件
前面我们使用了访问令牌和redis的形式代替session,也可以使用Spring提供的SpringSession实现session共享
开启SpringSession的步骤如下:
在service-user中添加spring-session依赖
org.springframework.session spring-session-data-redis
在service-util的RedisConfig中添加如下配置:将默认的jdk序列化方案修改为json序列化方案
@Bean //(name = "springSessionDefaultRedisSerializer")public RedisSerializer
上面的配置注入如下的位置
此时,之前我们存入session中的防止CSRF攻击的参数state,就可以自动存储在redis中了
在FrontWxController中添加测试代码
@GetMapping("testSaveSession") //在8203执行session存储public void testSaveSession(HttpSession session){ session.setAttribute("user", "helen");}@GetMapping("testGetSession") //在8213执行session获取public void testGetSession(HttpSession session){ String user = (String)session.getAttribute("user"); log.info(user);}
源码:https://gitee.com/dengyaojava/guigu-syt-parent
标签:
上一篇: 世界热文:果然是油画大师,亨利·阿森西奥画的美女人体,惊艳中更具朦胧美!
下一篇: 最后一页
全球视讯!尚医通-day10【微信扫码登录】(内附源码)
世界热文:果然是油画大师,亨利·阿森西奥画的美女人体,惊艳中更具朦胧美!
全球最资讯丨日经指数收盘突破33000点,近33年来首次,科技和汽车股领涨
当前速讯:你怎么知道梗含义一览
魂锁典狱长技能加点(LOL魂锁典狱长出装辅助攻略)|环球视讯
70岁老人保险,哪种好,哪个值得买?
天天热头条丨国家助学贷款累计发放超4000亿元 惠及2000多万名学生
ChatGPT当神父火了!数百人参会,排队1小时听它布道
新编名歌合唱曲集_关于新编名歌合唱曲集简介
环球速看:《双向奔赴》——关爱新就业形态劳动者⑤:暖途
ST股票对于本股神来说太简单了|要闻速递
国家助学贷款累计发放超4000亿元 惠及2000多万名学生
中国十七冶集团郑蒲港智能装备制造产业园建设项目全力推进项目收尾工作_全球快看点
威海市环翠区水利局开展2023年山洪灾害防治暨水利防汛抢险应急演练
广西父亲带女儿跑步200余天后孩子判若两人,获网民激赞
世界热文:电池30ETF:融资净偿还18.18万元,融资余额960.64万元(06-12)
和嘉控股发盈喜 预期本财年除税前收益16亿港元 天天关注
又一世界500强制造基地在沣东新城投产_精选
生物工程类制药工业水污染物排放标准_关于生物工程类制药工业水污染物排放标准介绍
头条焦点:江苏双武机械设备有限公司_关于江苏双武机械设备有限公司简述
智云健康(09955.HK):6月12日南向资金减持4.18万股
宜宾o2o酒吧_宜宾吧吧客酒店管理有限公司_全球快消息
中信股份(00267.HK):中信金属(601061.SH)拟每10股派现金红利1.5元
【天天快播报】股价上演“11天连涨”!特斯拉“完胜对手”的超充网络有望“统一北美”?
海口购车摇号申请网站入口(网址+条件)-环球百事通
一个骗子的自述:我是如何盯上你的! 快看点
618空调没有大促,爆款卖到脱销
长三角问道科创金融:“情怀”之外,如何续航?
聚焦“名校+” 这份“成绩单”亮点多多|天天观速讯
林诗达_关于林诗达概略