谷粒学院
OAuth2的使用场景
一、OAuth2解决什么问题
1、OAuth2提出的背景
照片拥有者想要在云冲印服务上打印照片,云冲印服务需要访问云存储服务上的资源
2、图例
资源拥有者:照片拥有者
客户应用:云冲印
受保护的资源:照片
3、方式一:用户名密码复制
直接给密码,让他访问
适用于同一公司内部的多个系统,不适用于不受信的第三方应用
4、方式二:通用开发者key
开发通用钥匙
适用于合作商或者授信的不同业务部门之间
5、方式三:办法令牌
按照特定的规则,生成一个字符串,再将字符串颁发给访问者,并可以设置字符串的有效时间,与管理字符串,如吊销等,访问者拿着生成的字符串就可以去进行访问
接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议
二、现代微服务安全
除了开放系统授权,OAuth2还可以应用于现代微服务安全
1、传统单块应用的安全
2、现代微服务安全
现代微服务中系统微服务化以及应用的形态和设备类型增多,不能用传统的登录方式
核心的技术不是用户名和密码,而是token,由AuthServer颁发token,用户使用token进行登录
3、典型的OAuth2应用
三、总结
四、OAuth2最简向导
川崎高彦:OAuth2领域专家,开发了一个OAuth2 sass服务,OAuth2 as Service,并且做成了一个公司再融资的过程中为了向投资人解释OAuth2是什么,于是写了一篇文章,《OAuth2最简向导》
OAuth2的定义
一、什么是OAuth2
记住:
OAuth2是一种授权框架,按照一定规则生成字符串,字符串包含用户信息,但是,他不提供具体的
生成规则,他是一种解决方案
,主要解决:1、开放系统间授权 ,2、分布式访问问题
1、OAuth2正式定义
2、令牌的核心
3、OAuth2的历史
4、OAuth2的优势
5、OAuth2的不足
6、Auth2涉及的角色
7、OAuth2术语
8、OAuth2令牌的类型
9、OAuth2的误解
二、回顾
微信二维码生成授权URL
一、准备工作
https://open.weixin.qq.com
1、注册
2、邮箱激活
3、完善开发者资料
4、开发者资质认证
准备营业执照,1-2个工作日审批、300元
5、创建网站应用
提交审核,7个工作日审批
6、熟悉微信登录流程
参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=e547653f995d8f402704d5cb2945177dc8aa4e7e&lang=zh_CN
获取access_token时序图
二、后端开发
1、添加配置
将固定的那个id 秘钥放在配置文件中,然后进行读取
# 微信开放平台 appid
wx.open.app_id=你的appid
# 微信开放平台 appsecret
wx.open.app_secret=你的appsecret
# 微信开放平台 重定向url
wx.open.redirect_url=http://你的服务器名称/api/ucenter/wx/callback
redirect_url指的是扫码后重定向的url
2、创建常量类
创建util包,创建ConstantPropertiesUtil.java常量类,读取上面的配置数据
@Component
//@PropertySource("classpath:application.properties")
public class ConstantProperties implements InitializingBean {
@Value("${wx.open.app_id}")
private String appID;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_APP_ID;
public static String WX_APP_SECRET;
public static String WX_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_APP_ID=this.appID;
WX_APP_SECRET=this.appSecret;
WX_REDIRECT_URL=this.redirectUrl;
}
}
3、创建controller
guli-microservice-ucenter微服务中创建api包
api包中创建WxApiController
@Controller //注意这里没有配置 @RestController
@CrossOrigin
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
//生成微信扫描二维码, %s 相当于?占位符
@GetMapping("/login")
public String getWxCode(){
String baseUrl ="https://open.weixin.qq.com/connect/qrconnect"
+"?appid=%s"
+"&redirect_uri=%s"
+"&response_type=code"
+"&scope=snsapi_login"
+"&state=%s"
+"#wechat_redirect";
//对redirect_uri进行URLEncoder编码
String redirect_uri = ConstantProperties.WX_REDIRECT_URL;
try {
redirect_uri = URLEncoder.encode(redirect_uri, "utf-8");//参数1:待编码字符串 参数2:编码方式
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//设置 %s 占位符的参数,上面有3处
String url = String.format(baseUrl,
ConstantProperties.WX_APP_ID,
redirect_uri,
"achang");
//请求微信地址
return "redirect:" + url;
}
}
授权url参数说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C46Z4eue-1615040671221)(http://qpgf4uqra.hn-bkt.clouddn.com/20210306152122.png)]
4、测试
访问:http://localhost:8006/api/ucenter/wx/login
访问授权url后会得到一个微信登录二维码
用户扫描二维码会看到确认登录的页面
用户点击“确认登录”后,微信服务器会向谷粒学院的业务服务器发起回调,因此接下来我们需要开发回调controller
注意:如果没有正确的配置业务服务器的回调url,则会看到以下错误提示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYBRK2Gp-1615040671224)(http://qpgf4uqra.hn-bkt.clouddn.com/20210306152330.png)]
微信登录开发回调URL
一、准备工作
用于测试,实际开发中,只需要修改下面的redirect_url的值指定跳转即可
这里是尚硅谷为了让我们测试到效果,所以指定了跳转到本地的8150端口的/api/ucenter/wx/callback路径
所以根据这个,下面就在controller中写一个这样子的方法用于扫码后跳转测试
1、全局配置的跳转路径
# 微信开放平台 重定向url
wx.open.redirect_url=http://回调地址/api/ucenter/wx/callback
redirect_url指的是扫码后重定向的url
2、修改当前项目启动端口号为8150
# 服务端口
#server.port=8006
server.port=8150
3、测试回调是否可用
在WxApiController中添加方法
@GetMapping("/callback")
public String callback(String code, String state, HttpSession session){
//得到授权临时票据code
System.out.println("code = " + code);
System.out.println("state = " + state);
return "redirect:http://localhost:3000";
}
- 测试效果
二、后台开发
1、添加依赖
<dependencies>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
2、创建httpclient工具类
放入util包
HttpClientUtils.java
3、创建回调controller方法
在WxApiController.java中添加如下方法
package com.achang.serviceUcenter.controller;
import com.achang.commonutils.JwtUtils;
import com.achang.serviceUcenter.entity.UcenterMember;
import com.achang.serviceUcenter.service.UcenterMemberService;
import com.achang.serviceUcenter.utils.ConstantProperties;
import com.achang.serviceUcenter.utils.HttpClientUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
/******
@author 阿昌
@create 2021-03-06 14:48
*******
*/
@Controller //注意这里没有配置 @RestController
@CrossOrigin
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
@Autowired
private UcenterMemberService ucenterMemberService;
@GetMapping("/callback")
public String callback(String code, String state, HttpSession session){
//获取code值,临时票据,类似于验证码
//拿着code,去请求微信固定的地址,得到两个值 access_token 和 openid
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token"+
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//拼接三个参数:id 秘钥 和 code值
String accessTokenUrl = String.format(baseAccessTokenUrl,
ConstantProperties.WX_APP_ID
, ConstantProperties.WX_APP_SECRET
, code);
//请求上面拼接好的地址,得到两个值 access_token 和 openid
//使用httpclient【不用浏览器,也能模拟器出浏览器的请求和响应过程】发送请求,得到返回的结果
String accessTokenInfo = null;
try {
accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
System.out.println("accessTokenInfo:。。。" +accessTokenInfo);
} catch (Exception e) {
e.printStackTrace();
}
//从accessTokenInfo中获取出 access_token 和 openid 的值
//将 accessTokenInfo 转换成 map集合,根据map的key 就可以获取对应的value
//使用json转换工具
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String) mapAccessToken.get("access_token");
String openid = (String) mapAccessToken.get("openid");
//判断数据库是否存在相同的微信内容
UcenterMember member = ucenterMemberService.getMemberByOpenId(openid);
//如果没有,则为新用户,添加数据库
if (member == null){
//拿着 access_token 和 openid 的值再去请求微信提供的固定地址
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
String resultUserInfo = null;
try {
resultUserInfo = HttpClientUtils.get(userInfoUrl);
System.out.println(resultUserInfo);
} catch (Exception e) {
e.printStackTrace();
}
HashMap<String, Object> resultMap = gson.fromJson(resultUserInfo, HashMap.class);
String nickname = (String) resultMap.get("nickname");
String headimgurl = (String) resultMap.get("headimgurl");
//向数据库插入一条记录
member = new UcenterMember();
member.setNickname(nickname);
member.setAvatar(headimgurl);
member.setOpenid(openid);
ucenterMemberService.save(member);
}
//使用jwt根据member对象生成token字符串
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
//最后,返回首页,通过路径传递token字符串
return "redirect:http://localhost:3000?token="+jwtToken;
}
}
上面最后整合了JWT生成token字符串
- 测试
前端整合和显示用户登录信息
- 前端获取路径的代码
1、修改 default.vue页面脚本
<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";
import cookie from "js-cookie";
import loginApi from "@/api/login";
export default {
data() {
return {
token: "",
loginInfo: {
id: "",
age: "",
avatar: "",
mobile: "",
nickname: "",
sex: "",
},
};
},
created() {
//获取路径中token的值【用于微信二维码登录】
this.token = this.$route.query.token;
if (this.token) {
//判断路径中是否有token值
this.wxLogin();
}
this.showInfo();
},
methods: {
wxLogin() {
//设置token值
cookie.set("guli_token", this.token, {
domain: "localhost" });
cookie.set("guli_ucenter", "", {
domain: "localhost" });
//调用接口,根据token获取用户信息
loginApi.getLoginInfo().then((resp) => {
this.loginInfo = resp.data.data.userInfo;
cookie.set("guli_ucenter", this.loginInfo, {
domain: "localhost" });
});
},
logout() {
//清空cookie值
cookie.set("guli_token", "", {
domain: "localhost",
});
cookie.set("guli_ucenter", "", {
domain: "localhost",
});
//跳转页面到登录
this.$router.push({
path: "/login" });
},
//创建方法从cookie中获取信息
showInfo() {
//从cookie中获取信息
var userStr = cookie.get("guli_ucenter");
//转字符串转换成json对象(js对象)
if (userStr) {
this.loginInfo = JSON.parse(userStr);
}
},
},
};
</script>
- 测试
访问之前上面的二维码生成的链接
http://localhost:8150/api/ucenter/wx/login
扫码确认
讲师列表页
一、后端部分
1、TeacherFrontController
com.achang.eduservice.controller.front.TeacherFrontController
@RestController
@CrossOrigin
@RequestMapping("/eduservice/teacherFront")
public class TeacherFrontController {
@Autowired
private EduTeacherService eduTeacherService;
//前台系统分页查询讲师的方法
//page:当前页 ,limit:显示记录数
@PostMapping("/getTeacherFrontPageList/{page}/{limit}")
public R getTeacherFrontPageList(@PathVariable Long page,@PathVariable Long limit){
Page<EduTeacher> teacherPage = new Page<>(page, limit);
Map<String,Object> map = eduTeacherService.getTeacherFrontPageList(teacherPage);
//返回分页中的所有数据
return R.ok().data(map);
}
}
2、service
- 接口
public interface EduTeacherService extends IService<EduTeacher> {
......
//前台系统分页查询讲师的方法
Map<String, Object> getTeacherFrontPageList(Page<EduTeacher> teacherPage);
}
- Impl
@Service
public class EduTeacherServiceImpl extends ServiceImpl<EduTeacherMapper, EduTeacher> implements EduTeacherService {
//前台系统分页查询讲师的方法
@Override
public Map<String, Object> getTeacherFrontPageList(Page<EduTeacher> teacherPage) {
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
//把分页数据封装到pageTeacher对象中
baseMapper.selectPage(teacherPage,wrapper);
//把分页的数据获取出来返回一个map集合
HashMap<String, Object> map = new HashMap<>();
//总记录数
long total = teacherPage.getTotal();
//当前页
long current = teacherPage.getCurrent();
//每页记录数
long size = teacherPage.getSize();
//查询到的对象
List<EduTeacher> teacherList = teacherPage.getRecords();
//总页数
long pages = teacherPage.getPages();
//是否有上一页
boolean hasPrevious = teacherPage.hasPrevious();
//是否有下一页
boolean hasNext = teacherPage.hasNext();
//将数据封装到map中返回
map.put("total",total);
map.put("current",current);
map.put("size",size);
map.put("list",teacherList);
map.put("hasPrevious",hasPrevious);
map.put("hasNext",hasNext);
map.put("pages",pages);
return map;
}
}
- swagger测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c8oiWCuy-1615040671235)(http://qpgf4uqra.hn-bkt.clouddn.com/20210306195613.png)]
二、前端部分
1、创建api
创建文件夹api,api下创建teacher.js,用于封装讲师模块的请求
- guli-front\api\teacher.js
import request from '@/utils/request'
export default{
getPageList(page, limit){
return request({
url:`/eduservice/teacherFront/getTeacherFrontPageList/${
page}/${
limit}`,
method: 'get'
})
}
}
2、讲师列表组件中调用api
- pages/teacher/index.vue
这里采用的是另一种写法,效果跟之前的是一样的,看个人喜好
<script>
import teacherAPI from "@/api/teacher";
export default {
//异步调用,进页面调用,且只会调一次
//params:相当于之前的 this.$route.params.id 等价 params.id
//error:错误信息
asyncData({
params, error }) {
return teacherAPI.getPageList(1, 8).then((response) => {
//this.data = response.data.data
return {
data: response.data.data };
});
},
data() {
return {
};
},
methods: {
},
created() {
},
};
</script>
三、页面渲染
1、页面模板
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- 讲师列表 开始 -->
<section class="container">
<section class="c-sort-box unBr">
<div>
<!-- 无数据提示 开始-->
<!-- /无数据提示 结束-->
<!-- 数据列表 开始-->
<!-- /数据列表 结束-->
</div>
<!-- 公共分页 开始 -->
<!-- /公共分页 结束 -->
</section>
</section>
<!-- /讲师列表 结束 -->
</div>
</template>
2、无数据提示
添加:v-if=“data.total==0”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GtRZjkqo-1615040671237)(http://qpgf4uqra.hn-bkt.clouddn.com/20210306215939.png)]
3、有数据的情况
<article class="i-teacher-list" v-if="data.total > 0">
<ul class="of">
<li v-for="item in data.list" :key="item.id">
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a href="/teacher/1" :title="item.name" target="_blank">
<img :src="item.avatar" :alt="item.name" />
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a
href="/teacher/1"
:title="item.name"
target="_blank"
class="fsize18 c-666"
>{
{ item.name }}</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999"
>{
{item.intro}}</span
>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">{
{item.career}}</p>
</div>
</section>
</li>
</ul>
<div class="clear"></div>
</article>
4、测试
四、分页
1、分页方法
methods: {
//分页切换方法
//参数是页码数
gotoPage(page) {
teacherAPI.getPageList(page, 8).then((resp) => {
this.data = resp.data.data;
});
},
},
2、分页页面渲染
<!-- 公共分页 开始 -->
<div>
<div class="paging">
<!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="首页"
@click.prevent="gotoPage(1)"
>首页</a>
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="前一页"
@click.prevent="gotoPage(data.current - 1)"
><</a
>
<a
v-for="page in data.pages"
:key="page"
:class="{
current: data.current == page,
undisable: data.current == page,
}"
:title="'第' + page + '页'"
href="#"
@click.prevent="gotoPage(page)"
>{
{ page }}</a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="后一页"
@click.prevent="gotoPage(data.current + 1)"
>></a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="末页"
@click.prevent="gotoPage(data.pages)"
>末页</a
>
<div class="clear" />
</div>
</div>
<!-- 公共分页 结束 -->