SpringBoot整合OAuth2和Jwt实现第三方登录
一、OAuth2的使用场景
- 现代微服务中系统微服务化以及应用的形态和设备类型增多,不能用传统的登录方式
- 核心的技术不是用户名和密码,而是token,由AuthServer颁发token,用户使用token进行登录
二、OAuth详解
2.1 什么是OAuth
2.2 OAuth的优势
2.3 OAuth术语
2.4 OAuth2令牌的类型
三、和SpringBoot整合
3.1 准备工作
3.2 请求微信登录流程图
3.3 添加配置文件
# outh2相关配置
wx:
open:
#微信开放平台 appid
app_id: xxxxxxxxxxxxxxx
# 微信开放平台 appsecret
app_secret: xxxxxxxxxxxxxxxxx
# 微信开放平台 重定向url(guli.shop需要在微信开放平台配置)
redirect_url: http://xxxxxxxxxxx
3.4 将相关信息做成配置类
@Component
//@PropertySource("classpath:application.yml")
public class ConstantPropertiesUtil 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_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
3.5 定义请求登录方法
@GetMapping("/ucenterService/login")
@ApiOperation("生成微信扫描二维码")
public String genQrConnect(HttpSession session) {
LOGGER.info("生成微信扫描二维码 start ");
/* 固定地址,后面拼接参数
String url = "https://open.weixin.qq.com/" +
"connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID+"&response_type=code";*/
// 微信开放平台授权baseUrl %s相当于?代表占位符(此地址为固定地址,固定参数 )
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 回调地址
// 获取业务服务器重定向地址(需要在微信开放平台配置)
String redirectUrl = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
try {
//对redirect_url进行URLEncoder编码
redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(20001, e.getMessage());
}
// 防止csrf攻击(跨站请求伪造攻击)
//String state = UUID.randomUUID().toString().replaceAll("-", "");//一般情况下会使用一个随机数
// 这里填写你在ngrok的前置域名
String state = "xxxx";
LOGGER.info("state = {}", state);
// 采用redis等进行缓存state 使用sessionId为key 30分钟后过期,可配置
//键:"wechar-open-state-" + httpServletRequest.getSession().getId()
//值:satte
//过期时间:30分钟
// 生成qrcodeUrl
String qrcodeUrl = String.format(
baseUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
redirectUrl,
state
);
LOGGER.info("生成微信扫描二维码 end 重定向地址为:{}", qrcodeUrl);
return "redirect:" + qrcodeUrl;
}
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 应用唯一标识 |
redirect_uri | 是 | 请使用urlEncode对链接进行处理 |
response_type | 是 | 填code |
scope | 是 | 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即 |
state | 否 | 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验 |
请求登录的流程大致是这样子的:
- 首先请求一个固定的地址(该地址由微信官方提供,我们只需要按照要求提供相应的参数来调用即可)
- 为了安全性,redirect_url需要进行URLEncoder编码
- 请求完成后,微信会返回给我们一个地址,我们只需要重定向到该地址,就可以在该页面看到相应的二维码信息。
- 在我们扫码点击登录后,微信官方就会将我们的请求转发到我们的回调方法上(需要自己配置),我们在回调方法上来进行一些数据的操作。
3.6 回调方法
@GetMapping("callback")
@ApiOperation(value = "获取扫描人信息,添加数据", tags = "当用户扫描二维码登录后,会回调到本方法中")
public String callback(String code, String state, HttpSession session) {
LOGGER.info("用户微信扫描登陆之后开始进行回调: code = {},state = {}", code, state);
// 从redis中将state获取出来,和当前传入的state作比较(正确的做法)
// 如果一致则放行,如果不一致则抛出异常:非法访问(正确的做法)
try {
// 1 获取code值,临时票据,类似于验证码
// 2 拿着code请求 微信固定的地址,得到两个值 accsess_token 和 openid
// 向认证服务器发送请求换取access_token
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,
ConstantWxUtils.WX_OPEN_APP_ID,
ConstantWxUtils.WX_OPEN_APP_SECRET,
code);
// 请求这个拼接好的地址,得到返回两个值 accsess_token 和 openid
// 使用httpclient发送请求,得到返回结果
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
LOGGER.info("accessTokenInfo = {}", accessTokenInfo);
// 从accessTokenInfo字符串获取出来两个值 accsess_token 和 openid
// 把accessTokenInfo字符串转换map集合,根据map里面key获取对应值
// 使用json转换工具 Gson
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String accsess_token = (String) mapAccessToken.get("access_token");
String openid = (String) mapAccessToken.get("openid");
LOGGER.info("成功得到accsess_token和openid accsess_token = {},openid = ", accsess_token, openid);
// 把扫描人信息添加数据库里面
// 判断数据表里面是否存在相同微信信息,根据openid判断(openid是唯一的)
xXXXX XXX = xxxxService.getOpenId(openid);
// 之前没有用微信登陆过
if (null == XXXX) {
LOGGER.info("当前用户尚未使用过微信登录,将会该用户的信息存入数据库");
// 3 拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息
// 访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//拼接两个参数
String userInfoUrl = String.format(
baseUserInfoUrl,
accsess_token,
openid
);
// 发送请求,得到微信官方返回的用户信息
String userInfo = HttpClientUtils.get(userInfoUrl);
LOGGER.info("微信官方返回的用户信息为:{}", userInfo);
//获取返回userinfo字符串扫描人信息
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = (String) userInfoMap.get("nickname");//昵称
String headimgurl = (String) userInfoMap.get("headimgurl");//头像
//将用户的头像信息和昵称存入数据库 略
LOGGER.info("成功将微信信息存入数据库中,存入的信息为:{}", member);
}
LOGGER.info("当前用户已经使用过微信进行登录,直接返回该用户的信息");
// TODO 登录
// 生成jwt
String token = JwtUtils.getJwtToken(xxx.getId(), xxx.getNickname());
// 存入cookie
//CookieUtils.setCookie(request, response, "guli_jwt_token", token);
// 因为端口号不同存在跨域问题,cookie不能跨域,所以这里使用url重写
LOGGER.info("微信登陆成功后重定向到首页,携带的token信息为:{}", token );
return "redirect:http://xxx:xxxx?token=" + token;
} catch (Exception e) {
LOGGER.error("微信登录失败,异常信息为:{}", e.toString());
throw new RunTimeException(20001, "登录失败");
}
}
回调方法简介
- 首先我们可以直接在请求参数中添加code 和 state,这两个信息就类似于验证码,我们在通过这两个数据去请求微信固定的地址,得到两个值accsess_token 和 openid 微信返回的数据为JSON格式,我们可以通过fastJson或其他来将数据转换为Map集合,在通过map集合来取到其中的这两个数据。
- 然后在通过accsess_token 和 openid这两个数据请求另一个微信官方的地址来获取微信账号的信息(包括微信账号的头像地址和微信名称)
- 获取到这些信息之后,便可一通过openid(唯一标识)进行判断,来判断用户之前是否使用过微信登录,如果没有登陆过,就可以将用户信息保存到数据库中。
- 使用JWT公具类来根据用户的openid和微信昵称来生成token
- 微信登陆成功后重定向到到目标页面,并且携带token信息
- 前端页面从请求路径中达到token信息,将token存储在cookie或者请头头中。
- 用户携带token请求用户信息,后端解析通过,将用户信息返回给前端,前端将用户信息作展示。
3.7 前端页面拦截请求路径的token并存储在cookie中
export default {
data() {
return {
token: '',
}
},
created() {
this.token = this.$route.query.token
if (this.token) {
this.wxLogin()
}
},
methods: {
wxLogin() {
if (this.token == '') return
//把token存在cookie中、也可以放在localStorage中
cookie.set('guli_token', this.token, {domain: 'localhost'})
cookie.set('guli_ucenter', '', {domain: 'localhost'})
//登录成功根据token获取用户信息
userApi.getLoginInfo().then(response => {
this.loginInfo = response.data.data.item
//将用户信息记录cookie
cookie.set('guli_ucenter', this.loginInfo, {domain: 'localhost'})
})
}
}
}
其中涉及到的jwt工具类和HttpClientUtils可以私聊我,我私发给你