单点登录-第三方对接OAuth2.0
大家好,你是不是经常遇到这样的烦恼:每次想登录一个新的网站或者应用,都要重新输入用户名和密码,有时候还得设置各种密保问题,简直烦死个人了!
其实啊,有一个叫做“单点登录”的东西,就像是给你准备了一把“万能钥匙”,只要有了它,你就可以轻松打开多个应用的大门,再也不用为登录发愁了。而在这个“万能钥匙”里,有一个非常流行的“零件”叫做OAuth2.0,它能让第三方应用轻松对接到你的账户,让你在享受各种服务的同时,还能保证账号的安全。在这篇文章里,我会用最简单的话,给大家讲讲单点登录和OAuth2.0是怎么一回事,怎么用它来对接第三方应用,让我们的在线生活更加便捷和安全。哪怕你是个技术小白,也能看得懂、学得会!大家一起来学习进步(作者也是小白>.<)
所以,如果你也对这个神奇的“万能钥匙”感兴趣,就跟我一起探索吧!相信我,学完之后,你会觉得登录这事儿,简直变得轻松又有趣啦!如果本文有什么还不太了解的或者聪明的大家有什么需要补充的也可以在评论区留言
单点登录
对接第三方
什么是OAuth协议
OAuth协议为用户资源的授权提供了一个安全又简易的标准。与以往的授权方式不同之处是
OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此
OAuth是安全的。 OAuth是 Open Authorization的简写
OAuth本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现。
其步骤一般如下
- 第三方(客户端)要求用户给予授权
- 用户同意授权
- 根据上一步获得的授权,第三方向认证服务器请求令牌( token)
- 认证服务器对授权进行认证,确认无误后发放令牌
- 第方使用令牌向资源服务器请求资源
- 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源
OAuth2.0是为了解决什么问题?
任何身份认证,本质上都是基于对请求方的不信任所产生的。同时,请求方是信任被请求方的,例如用户请求服务时,会信任服务方。所以,
身份认证就是为了解决 身份的可信任问题。
在 OAuth2.0中,简单来说有三方:
- 用户(这里是指属于 服务方的用户)
- 服务方(如微信、微博等)
- 第三方应用(也就是所谓的客户端)
服务方不信任 用户,所以需要用户提供密码或其他可信凭据(用户需要用户密码登录) 服务方不信任
第三方应用,所以需要第三方提供自已交给它的凭据(如微信授权的 code,AppID等) 用户部分信任
第三方应用,所以用户愿意把自已在服务方里的某些服务交给第三方使用,但不愿意把自已在服务方的密码等交给第三方应用
OAuth2.0成员和授权基本流程
OAuth2.0成员:
- Resource Owner(资源拥有者:用户)
- Client (第三方接入平台:请求者)
- Resource Server (服务器资源:数据中心)
- Authorization Server (认证服务器)
OAuth2.0基本流程(步骤详解):
- Authorization Request, 第三方请求用户授权
- Authorization Grant,用户同意授权后,会从服务方获取一次性用户 授权凭据(如 code码)给第三方Authorization Grant
- 第三方会把 授权凭据以及服务方给它的的 身份凭据(如 AppId)一起交给服务方的向认证服务器申请 访问令牌Access Token,认证服务器核对授权凭据等信息,确认无误后,向第三方发送 访问令牌 Access Token等信息Access Token
- 通过这个 Access Token向 Resource Server索要数据Protected Resource
- 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源
这样服务方,一可以确定第三方得到了用户对此次服务的授权(根据用户授权凭据),二可以确定第三方的身份是可以信任的(根据身份凭据),所以,最终的结果就是,第三方顺利地从服务方获取到了此次所请求的服务从上面的流程中可以看出, OAuth2.0完整地解决了 用户、 服务方、 第方在某次服务时这者之间的信任问题
SpringBoot整合OAuth2.0
略(开发中ing...写完以后会在这里粘贴进来的)
整合钉钉扫码登录–已更新完
一.构造扫码登录页面
2.进入应用界面–配置回调域名。
进入已创建的应用详情页,在基础信息页面可以查看到应用的SuiteKey/SuiteSecret(第三方企业应用)或AppKey/AppSecret(企业内部应用)。
在应用详情页,然后单击钉钉登录与分享,添加应用回调的URL,以http或https开头。
官方示范使用的是frp内网穿透达到模拟公网访问的效果我这里是直接使用的nataapp操作起来比较方便一些
3.构造扫码登录页面。
Web系统可以通过两种方式实现钉钉扫码登录。
方式一:使用钉钉提供的扫码登录页面
在企业Web系统里,用户点击使用钉钉扫描登录时第三方Web系统跳转到如下地址:
https://oapi.dingtalk.com/connect/qrconnect?appid=SuiteKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
url里的参数需要换成第三方Web系统对应的参数。在钉钉用户扫码登录并确认后,会302到你指定的redirect_uri,并向url参数中追加临时授权码code及state两个参数。
重要
参数redirect_uri=REDIRECT_URI涉及的域名,需和登录配置的回调域名一致,否则会提示无权限>访问。
如果是企业内部应用,appid则为应用的AppKey;如果是第三方企业应用,appid则为应用的SuiteKey。
方式二:支持网站将钉钉登录二维码内嵌到自己页面中
用户使用钉钉扫码登录后JS会将loginTmpCode返回给网站。
JS钉钉登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到钉钉域下登录后再返回,提升钉钉登录的流畅性与成功率。
网站内嵌二维码钉钉登录JS实现办法:
1.在页面中先引入如下JS文件(支持HTTPS)
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
2.在需要使用钉钉登录的地方实例以下JS对象
/*
* 解释一下goto参数,参考以下例子:
* var url = encodeURIComponent('http://localhost.me/index.php?test=1&aa=2');
* var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=SuiteKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='+url)
*/
var obj = DDLogin({
id:"login_container",//这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
goto: "", //请参考注释里的方式
style: "border:none;background-color:#FFFFFF;",
width : "365",
height: "400"
});
参数 | 说明 |
---|---|
goto | goto参数结构:https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=SuiteKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI,并且要将goto参数urlencode编码。 |
style | 渲染二维码的区域的样式,可以自定义去除背景颜色和边框。 |
width | 表示显示二维码的区域的宽。width和height不代表二维码的大小,二维码大小是固定的210px*210px。 |
height | 表示显示二维码的区域的高。width和height不代表二维码的大小,二维码大小是固定的210px*210px。 |
引入的js会在获取用户扫描之后将获取的loginTmpCode通过window.parent.postMessage(loginTmpCode,‘*’);返回给您的网站。
1.可以通过以下代码获取loginTmpCode:
var handleMessage = function (event) {
var origin = event.origin;
console.log("origin", event.origin);
if( origin == "https://login.dingtalk.com" ) { //判断是否来自ddLogin扫码事件。
var loginTmpCode = event.data;
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
console.log("loginTmpCode", loginTmpCode);
}
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', handleMessage, false);
} else if (typeof window.attachEvent != 'undefined') {
window.attachEvent('onmessage', handleMessage);
}
2.通过JS获取到loginTmpCode后,需要构造并跳转到如下链接:
https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=SuiteKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode=loginTmpCode
3.此链接处理成功后,会302跳转到goto参数指定的redirect_uri,并向url参数中追加临时授权码code及state参数
参数 | 是否必填 | 说明 |
---|---|---|
appid | √ | 1.如果是企业内部应用,appid则为应用的AppKey。2.如果是第三方企业应用,appid则为应用的SuiteKey。 |
redirect_uri | √ | 重定向地址。必须与开发者后台设置的回调域名保持一致。1.如果是第一种方式需要urlencode编码。2.如果是第二种方式则需要将JS goto参数整体urlencode编码,不要单独对redirect_uri编码,该地址为步骤一中配置的回调域名。 说明:重定向地址建议使用前端页面,否则可能出现重复追加code值的问题。 |
state | √ | 用于防止重放攻击,开发者可以根据此信息来判断redirect_uri只能执行一次来避免重放攻击。 |
response_type | √ | 固定为code。 |
scope | √ | 固定为snsapi_login。 |
loginTmpCode | √ | 通过js获取到的loginTmpCode。 |
3.服务端通过临时授权码获取授权用户的个人信息。
调用sns/getuserinfo_bycode接口获取授权用户的个人信息,详情请参考根据sns临时授权码获取用户信息。
4.根据unionid获取userid。
调用user/getbyunionid接口获取userid,详情请参考根据unionid获取用户userid。
5.根据userid获取用户详情。
调用user/get接口获取用户信息,详情请参考查询用户详情。
代码
参考网上若依开源项目改动而来使用的是vue2 不是3哦
前端代码
vue页面 login.vue
<template>
<div class="login-container">
<!-- 主要页面 -->
<div class="login-weaper" v-if="loginif">
<!--左边布局-->
<div class="login-left">
<p class="title">测试登录</p>
</div>
<!--右边布局-->
<div class="login-border">
<!--右上角图标-->
<div class="right-top" @click="codeLogin" v-if="isHaveQRCode">
<img src="../assets/image/code.png" v-if="!isCodeLogin"/>
<img src="../assets/image/code2.jpg" v-else/>
</div>
<!-- 二维码登录 -->
<div id="login_code" v-show="isCodeLogin"></div>
<!--账号密码登录-->
<div class="login-main" v-show="!isCodeLogin">
<div class="login-title">用户登录</div>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
size="medium"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="用户名"
>
<i class="el-icon-user" slot="prefix"></i>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon
slot="prefix"
icon-class="password"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 60%"
@keyup.enter.native="handleLogin"
>
<svg-icon
slot="prefix"
icon-class="validCode"
class="el-input__icon input-icon"
/>
</el-input>
<!--验证码-->
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox
v-model="loginForm.rememberMe"
style="margin: 0px 0px 25px 0px"
>记住密码
</el-checkbox
>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width: 100%; padding: 12px 20px"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
<div class="login_box" v-else>
<span class="login_text">登录中请等待</span>
<span v-loading="loading" class="loading"></span>
</div>
</div>
</template>
<script>
import { getConfig } from '@/api/demotoken/index'
import { getCodeImg } from '@/api/login'
import Cookies from 'js-cookie'
import { encrypt, decrypt } from '@/utils/jsencrypt'
export default {
name: 'Login',
data() {
return {
codeUrl: '',
cookiePassword: '',
loginForm: {
username: '',//账号
password: '',//密码
rememberMe: false,//是否记住密码
code: '',//输入框验证码
uuid: ''//图片验证码信息
},
loginRules: {
username: [
{ required: true, trigger: 'blur', message: '用户名不能为空' }
],
password: [
{ required: true, trigger: 'blur', message: '密码不能为空' }
],
code: [
{ required: true, trigger: 'change', message: '验证码不能为空' }
]
},
loading: false,
redirect: undefined,
loginif: true,
//二维码登录
isCodeLogin: false,
//是否设置二维码
isSet: false,
//是否显示右上角登录方式
isHaveQRCode: false
}
},
/*监听器*/
watch: {
//监听当前路由的对象
$route: {
//当路由发生变化的时候handler就会被调用 这段代码主要是为了登录后,恢复至重定向之前的页面。
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
computed: {},
//模块渲染后进行调用
mounted() {
//code是登录所需最终参数 判断当前的路由有没有携带关于code参数 有的话直接进行登录阶段
if (this.$route.query && this.$route.query.code) {
//登录接口
this.handleCodeLogin(this.$route.query.code)
} else {
// this.ddLogin();
}
},
//模块渲染之前进行调用
created() {
this.getCode()//生成验证码
this.getCookie()//获取当前cookie的信息
this.isHaveQRCode = true
this.isCodeLogin = true
//获取钉钉接口需要的参数
getConfig().then((res) => {
//获取会话中的值
window.sessionStorage.setItem('appkey', res.data.appkey)
window.sessionStorage.setItem('agentId', res.data.agentId)
window.sessionStorage.setItem('corpId', res.data.corpId)
if (res.data.accessKey) {
//钉钉扫码登录appid
window.sessionStorage.setItem('accessKey', res.data.accessKey)
//钉钉二维码
this.$nextTick((next) => {
if (this.isCodeLogin && !this.isSet) {
this.ddLogin()
}
})
}
})
},
methods: {
//钉钉二维码生成
ddLogin() {
let url = 'http://' + location.host + '/login'
let appid = window.sessionStorage.getItem('accessKey')
let goto = `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${appid}&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=${url}`
//生成钉钉二维码图片
var obj = DDLogin({
id: 'login_code', //这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
goto: encodeURIComponent(goto), //请参考注释里的方式
style: 'border:none;margin: 0 auto; background-color:inherit;',
width: '500',
height: '300'
})
//获取loginTmpCode
var handleMessage = function(event) {
var origin = event.origin
if (origin == 'https://login.dingtalk.com') {
//判断是否来自ddLogin扫码事件。
var loginTmpCode = event.data
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
//此步拿到临时loginTmpCode换取正式code 当前页面打开URL页面
window.location.href = `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${appid}&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=${url}&loginTmpCode=${loginTmpCode}`
}
}
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', handleMessage, false)
} else if (typeof window.attachEvent != 'undefined') {
window.attachEvent('onmessage', handleMessage)
}
this.isSet = true
},
/**
* 登录
* @param code免登code 后端会根据code来返回token
*/
handleCodeLogin(code) {
this.loginif = false
let self = this
this.$store.dispatch('getScanLogin', code).then(() => {
self.$router.push({ path: '/' })
})
.catch(() => {
})
},
//更换二维码登录
codeLogin(res) {
this.isCodeLogin = !this.isCodeLogin
},
/**
* 生成验证码
*/
getCode() {
getCodeImg().then((res) => {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.loginForm.uuid = res.uuid
})
},
/**
* 获取Cookies里面的信息
*/
getCookie() {
const username = Cookies.get('username')
const password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password:
password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true
if (this.loginForm.rememberMe) {
Cookies.set('username', this.loginForm.username, { expires: 30 })
Cookies.set('password', encrypt(this.loginForm.password), {
expires: 30
})
Cookies.set('rememberMe', this.loginForm.rememberMe, {
expires: 30
})
} else {
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
}
this.$store
.dispatch('Login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/' })
})
.catch(() => {
this.loading = false
this.getCode()
})
}
})
}
}
}
</script>
<style scoped lang="scss">
.login-container {
display: flex;
align-items: center;
-webkit-box-align: center;
width: 100%;
margin: 0 auto;
background: url("../assets/image/login.png") no-repeat;
background-color: #304175;
position: relative;
background-size: cover;
height: 100vh;
background-position: 50%;
#particles {
z-index: 1;
width: 100%;
height: 100%;
position: absolute;
}
.login-weaper {
width: 1000px;
margin: 0 auto;
box-shadow: -4px 5px 10px rgba(0, 0, 0, 0.4);
z-index: 1000;
.login-left {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
-webkit-box-pack: center;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
background-color: rgba(64, 158, 255, 0);
color: #ffffff;
float: left;
width: 50%;
position: relative;
min-height: 500px;
-webkit-box-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.login-time {
position: absolute;
left: 25px;
top: 25px;
width: 100%;
color: #ffffff;
opacity: 0.9;
font-size: 18px;
overflow: hidden;
font-weight: 500;
}
.img {
width: 120px;
height: 120px;
border-radius: 3px;
}
.title {
text-align: center;
color: #ffffff;
letter-spacing: 2px;
font-size: 30px;
font-weight: 600;
}
}
.login-border {
display: flex;
align-items: center;
position: relative;
min-height: 500px;
-webkit-box-align: center;
border-left: none;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
color: #ffffff;
background-color: hsla(0, 0%, 100%, 0.9);
width: 50%;
float: left;
.right-top {
position: absolute;
top: 0;
right: 0;
width: 53px;
height: 53px;
border-top-right-radius: 5px;
> img {
border-top-right-radius: 5px;
}
}
.login-main {
margin: 0 auto;
width: 65%;
.login-title {
color: #333333;
margin-bottom: 40px;
font-weight: 500;
font-size: 22px;
text-align: center;
letter-spacing: 4px;
}
/deep/ .el-input__inner {
height: 50px;
line-height: 50px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: hsla(0, 0%, 100%, 0.8);
border-radius: 5px;
color: #454545;
padding-left: 35px;
}
/deep/ .el-input__prefix {
height: 100%;
line-height: 50px;
padding-left: 10px;
}
.login-code {
width: 35%;
height: 50px;
float: right;
> img {
width: 100%;
height: 100%;
cursor: pointer;
vertical-align: middle;
}
}
}
}
}
}
</style>
code.png
code2.jpg
@/api/notoken/index
import axios from 'axios'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000
})
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 获取钉钉接口需要的参数--
export function getConfig() {
return service({
url: '/api/user/getDemoParameters',
method: 'get'
})
}
@/api/login
import request from '@/utils/request'
// 扫码登录
export function getDemoLogin(code) {
return request({
url: `/api/user/getDemoLogin?code=${code}`,
method: 'post'
})
}
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
method: 'post',
data: data
})
}
// 获取用户详细信息
export function getInfo() {
return request({
url: '/getInfo',
method: 'get'
})
}
// 退出方法
export function logout() {
return request({
url: '/logout',
method: 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
method: 'get'
})
}
jsencrypt.js
import JSEncrypt from 'jsencrypt/bin/jsencrypt'
// todo 下面的???号可以参考密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = '???'
const privateKey = '???'
// 加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
user.js
import { login, logout, getInfo, newlogin ,getScanLogin} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
userid: '',
roles: [],
permissions: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_USERID: (state, userid) => {
state.userid = userid
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
}
},
actions: {
// 扫码的登录
getScanLogin({ commit }, code) {
//异步请求
return new Promise((resolve, reject) => {
//将结果保存到本次会话中
getScanLogin(code).then(res => {
window.sessionStorage.setItem("userinfo", JSON.stringify(res.data));
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(res => {
// //console.log("getInfo")
// //console.log(res)
const user = res.user
const avatar = user.avatar == "" ? require("@/assets/image/profile.jpg") : user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.nickName)
commit('SET_USERID', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
public/index.html
引入https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico"> -->
<script type="text/javascript"
src="https://webapi.amap.com/maps?v=1.4.15&key=c318869d45386c7a83f4622d1d9925fb"></script>
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
<title><%= webpackConfig.name %></title>
<style>
html,
body,
#app {
height: 100%;
margin: 0px;
padding: 0px;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
#loader {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-webkit-animation: spin 2s linear infinite;
-ms-animation: spin 2s linear infinite;
-moz-animation: spin 2s linear infinite;
-o-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
z-index: 1001;
}
#loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-webkit-animation: spin 3s linear infinite;
-moz-animation: spin 3s linear infinite;
-o-animation: spin 3s linear infinite;
-ms-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
#loader:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-moz-animation: spin 1.5s linear infinite;
-o-animation: spin 1.5s linear infinite;
-ms-animation: spin 1.5s linear infinite;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
#loader-wrapper .loader-section {
position: fixed;
top: 0;
width: 51%;
height: 100%;
background: #7171C6;
z-index: 1000;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
#loader-wrapper .loader-section.section-left {
left: 0;
}
#loader-wrapper .loader-section.section-right {
right: 0;
}
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%);
-ms-transform: translateX(-100%);
transform: translateX(-100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
}
.loaded #loader {
opacity: 0;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.loaded #loader-wrapper {
visibility: hidden;
-webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%);
transform: translateY(-100%);
-webkit-transition: all 0.3s 1s ease-out;
transition: all 0.3s 1s ease-out;
}
.no-js #loader-wrapper {
display: none;
}
.no-js h1 {
color: #222222;
}
#loader-wrapper .load_title {
font-family: 'Open Sans';
color: #FFF;
font-size: 19px;
width: 100%;
text-align: center;
z-index: 9999999999999;
position: absolute;
top: 60%;
opacity: 1;
line-height: 30px;
}
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #FFF;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="app">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div>
</div>
</div>
</body>
<script type="text/javascript">
//第一步,创建XMLHttpRequest对象
var xmlHttp = new XMLHttpRequest()
function CommentAll() {
//第二步,注册回调函数
xmlHttp.onreadystatechange = callback1
xmlHttp.open('get', '<%= process.env.VUE_APP_BASE_API %>/api/user/getSite', true)
xmlHttp.send('')//"
}
//第五步,创建回调函数
function callback1() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
//取得返回的数据
var data = xmlHttp.responseText
//json字符串转为json格式
data = JSON.parse(data)
document.title = data.data.siteName
var headHTML = document.getElementsByTagName('head')[0].innerHTML
headHTML += '<link rel="icon" href="data:image/jpeg;base64,' + data.data.siteLogo + '">'
document.getElementsByTagName('head')[0].innerHTML = headHTML
}
}
}
CommentAll()
</script>
</html>
有什么不对的可以在评论区留言,后期补充,我也是才接触第三方登录的一起学习一起进步ヾ(◍°∇°◍)ノ゙
后端代码
@ApiOperation("demo钉钉扫码")
@PostMapping("/getDemoLogin")
@ApiImplicitParam(name = "code", value = "免登code", required = true, dataType = "String")
@ResponseBody
public AjaxResult getDemoLogin(String code) throws ApiException {
AjaxResult ajaxResult = new AjaxResult();
OapiUserGetResponse scanDdUser = DdUtils.getDemoUser(code);
String token = loginController(scanDdUser);
ajaxResult.put("code", 0);
ajaxResult.put("msg", scanDdUser.getErrmsg());
ajaxResult.put("data", scanDdUser);
ajaxResult.put("token", token);
return ajaxResult;
}
@Component
public class DingDingUtils {
public static String getAccessToken() throws ApiException {
String appkey = "***";//这里的可以将对应的数据放在配置参数里面进行动态的获取
String appsecret = "***";//这里的可以将对应的数据放在配置参数里面进行动态的获取
DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest request = new OapiGettokenRequest();
request.setAppkey(appkey);
request.setAppsecret(appsecret);
request.setHttpMethod("GET");
OapiGettokenResponse response = client.execute(request);
return response.getAccessToken();
}
public static OapiUserGetResponse getDemoUser(String code) throws ApiException {
String accessToken = DdUtils.getAccessToken();
String accessKey = "***";
String accessSecret = "***";
// 通过临时授权码获取授权用户的个人信息
DefaultDingTalkClient client2 = new DefaultDingTalkClient("https://oapi.dingtalk.com/sns/getuserinfo_bycode");
OapiSnsGetuserinfoBycodeRequest reqBycodeRequest = new OapiSnsGetuserinfoBycodeRequest();
// 通过扫描二维码,跳转指定的redirect_uri后,向url中追加的code临时授权码
reqBycodeRequest.setTmpAuthCode(code);
OapiSnsGetuserinfoBycodeResponse bycodeResponse = client2.execute(reqBycodeRequest, accessKey, accessSecret);
// 根据unionid获取userid
String unionid = bycodeResponse.getUserInfo().getUnionid();
DingTalkClient clientDingTalkClient = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid");
OapiUserGetbyunionidRequest oapiUserGetbyunionIdRequest = new OapiUserGetbyunionidRequest();
oapiUserGetbyunionIdRequest.setUnionid(unionid);
OapiUserGetbyunionidResponse oapiUserGetbyunionidResponse = clientDingTalkClient.execute(oapiUserGetbyunionIdRequest, accessToken);
String userid = oapiUserGetbyunionidResponse.getResult().getUserid();
DingTalkClient client1 = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/get");
OapiUserGetRequest request1 = new OapiUserGetRequest();
request1.setUserid(userid);
request1.setHttpMethod("GET");
OapiUserGetResponse response = client1.execute(request1, accessToken);
return response;
}
}
public String loginService(SysUser user)
{
Set<String> permissions = sysMenuService.selectMenuPermsByUserId(user.getUserId());
LoginUser loginUser =new LoginUser();
loginUser.setPermissions(permissions);
loginUser.setUser(user);
// 生成token
String token = IdUtils.fastUUID();
loginUser.setToken(token);
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + 30 * 60 * 1000);
// 根据uuid将loginUser缓存
String userKey ="login_tokens:" + loginUser.getToken();;
redisCache.setCacheObject(userKey, loginUser, 60, TimeUnit.MINUTES);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
String token1 = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, "************").compact();
return token1;
}
public String loginController(OapiUserGetResponse ddUser) {
String token = null;
if (ddUser != null && StringUtils.isNotEmpty(ddUser.getUserid())) {
String userid = ddUser.getUserid();
//查询数据库该用户是不是存在的 钉钉的userid匹配数据库对应的人员
SysUser user1 = userService.selectUserByUserName(userid);
if (user1 != null && user1.getUserId() != null && user1.getUserId() > 0) {
// 老用户 生成令牌
token = loginService.loginService(user1);
} else {
// todo 根据当前钉钉返回的人员信息将当前的人员保存到数据库里面人员表 部门表 角色表 以及其他的一些关联表
}
}
return token;
}