OAuth2.0 第三方授权登录(微信和Gitee)
1. OAuth2.0 简介
全称:Open Authorization
OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方。 OAuth在全世界得到广泛应用,目前的版本是2.0版。
特点:
- 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解和使用
- 安全:没有涉及到用户密钥等信息,更安全灵活
- 开放:任何服务提供商都可以实现OAuth,任何软件开发上商都可以使用OAuth
四种模式
-
授权码模式(Authorization Code):OAuth2 中目前最安全最复杂也是最常用的模式
-
隐式授权模式(Implicit Grant):
-
用户命密码模式(Resource Owner Passowrd Credentials grant ):密码凭证授权
-
客户端模式(Client Credientials Grant):客户端授权
2. OAuth2.0 优点
OAuth登录的优点
- 第三方登录简单快捷,面对不同平台不同的用户名和密码的问题,第三方登 录正好解决这个问题,几乎可以直接一个账号搞定所有
- 第三方登录还可以将自己在某个应用的动态信息同步到当前应用下,无需再为每个应用重新写个人资料
- 第三方登录有很多资料信息可以公用(比如头像和昵称),通常对于敏感资料如手机、邮箱是第三方平台是不会提供的,所以安全信息可以放心
OAuth为企业带来的价值
-
简化登录过程,降低注册门槛,更能获取海量用户,有效降低了用户的流失。本地注册的稳定+第三方登 录的便捷才是最合适的登录方案
-
第三方登录接入后,应用可直接获取用户昵称、头像、用户D等信息,方便产品获取用户的基本资料,减少产品设计成本
-
目前市面上的短信验证码的价钱约在0.05元左右,当用户选择使用第三方登录时,可有效减少产品的登 录成本
OAuth为第三方提供商带来的价值
-
增加用户对平台(QQ\微信\支付宝\Google)的依赖,用户越多使用本平台的第三方登录,就代表着平台对该用户的粘性越高
-
获得更广泛的影响力,只要用户使用提供第三方登录的应用,那么这个提供第三方登录的品牌就会被用户浏览,有利于对平台的拉新和促活
3 OAuth2.0 角色
角色 | 作用 |
---|---|
客户端 | 本身不存储资源,需要通过资源拥有者去请求资源服务器的资源。APP,游戏,影视网站... |
资源拥有者 | 通常为用户,也可以是应用程序,即资源的 |
授权服务器 | 用于服务提供商对资源拥有者的身份进行认证、对访问资源进行授权发放授权码(code)。认证成功后通过授权码请求发放令牌(token),作为客户端访问资源服务器的凭据。 |
资源服务器 | 存储资源的服务器,比如微信端存储的用户信息 |
4 OAuth2.0 授权码模式
OAuth2 中目前最安全最复杂也是最常用的模式,执行流程如下
5 二维码生成
5.1 依赖要求
<!--Hutool:JaVa工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Ui工具类。-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
</dependency>
<!--生成二维码-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
5.2 生成二维码示例
- 普通生成:四个参数:Url,宽,高,输出路径
QrCodeUtil.generate(
"https://warriorsgo.netlify.app",300,300, FileUtil.file("d:/qrcode.jpg")
);
- 配置生成:配置信息较多时推荐将配置信息写入配置对象再生成
//配置参数较多时使用配置生成
QrConfig config = new QrConfig(1000, 1000);//新建配置文件并指定宽高
config.setErrorCorrection(ErrorCorrectionLevel.Q);//指定纠错级别
QrCodeUtil.generate(
"https://warriorsgo.netlify.app",config, FileUtil.file("d:/qrcode.jpg")
);
其中:纠错级别 ErrorCorrectionLevel有四个等级:L,M,Q,H
从左到右:纠错等级提高,即使被遮挡一小部分也能识别成功,单位像素块面积减小,像素块数量增多,
- 其他配置
配置方法 | 作用 |
---|---|
setBackColor(Color.XXX) | 设置背景颜色 |
setForeColor(Color.XXX) | 设置像素块颜色 |
setImg(ImgPath) | 设置中心logo图标 |
setMargin(margin) | 设置边缘宽度 |
setHeight(h)/setWidth(w) | 设置宽高 |
6 Cpolar 内网穿透工具
cpolar极点云: 公开一个本地Web站点至公网
作用:只需一行命令,就可以将内网站点发布至公网,方便给客户演示。高效调试微信公众号、小程序、对接支付宝网关等云端服务,提高您的编程效率。
- 登录注册下载安装
download下载 - cpolar 极点云:www.cpolar.com/download
- 获取cpolar账号Token
cpolar - secure introspectable tunnels to localhost:dashboard.cpolar.com/auth
- 命令行运行cpolar,验证token
cpolar authtoken MGU1OWY4NWItXXXXXXXXXXXXXXXX
- 实现简单内网穿透:cpolar 协议名 内网端口
cpolar http 8080
注意:每次重启后随机域名会改变!
Forwarding里面的域名就是公网域名,可以给其他外网设备访问。
7 OAuth2.0 - 微信登录
7.1 准备工作
扫码登陆微信有两种实现方式
-
基于微信公众平台的扫码登录
让第三方应用投入微信的怀抱而设计的,这第三方应用指的是比如android、ios、网站、系统等;
-
基于微信开放平台的扫码登录(For common people) 为了让程序员小伙伴利用微信自家技术(公众号、小程序)开发公众号、小程序而准备的。
区别 微信开放平台需要开企业认证才能注册。 微信公众平台需要认证微信服务号,才能进行扫码登录的开发。只需申请一个公众号。
对于初学者,即没有企业认证,也不一定有自己的公众号,就只能使用测试公众号
测试公众号申请地址:微信公众平台 (qq.com)
1. 接口配置信息:
参数 | 说明 |
---|---|
URL | 此处要加协议,以及对应的检验接口 |
token | 随意设置自定义token |
示例:
URL : 63c27f47.r7.cpolar.top/wechat/chec…
Token :AASAdd
2. 网页授权获取用户基本信息 - 修改回调域名
在本网页里面找到 - 网页服务 - 网页账号 - 网页授权获取用户基本信息 -修改:授权回调页面域名
-
回调域名作用:
用户在网页授权页同意授权给公众号后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠。沙盒号回调地址支持域名和ip,正式公众号回调地址只支持域名
-
注意:此处填写域名,不是URL!无需https:// 等协议头
示例:
回调地址:63c27f47.r7.cpolar.top
7.2 实现细节
基本步骤:
网页授权流程分为四步:
- 引导用户进入授权页面同意授权,获取code
- 授权页面可以以二维码或者前端请求跳转的形式给用户
- 授权链接参数解释:网页授权 | 微信开放文档 (qq.com)
- 通过code换取网页授权access token(与基础支持中的access_token不同)
- 如果需要,开发者可以刷新网页授权access_token,避免过期
- 通过网页授权access token和openid获取用户基本信息(支持UnionID机制)
7.2.0 微信接口校验
编写微信验证的测试接口方法:
@Controller
@RequestMapping("/wechat")
public class WeChatController {
@GetMapping("/check")
@ResponseBody
public String WXCheck(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timeStamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echoStr){
System.out.println("hello");
System.out.println("echostr = " + echoStr);
return echoStr;
}
...
}
developers.weixin.qq.com/doc/offiacc…
完成 7.1 准备工作中的 URL,TOKEN,回调域名 的数据填写,配置后点击测试,显示 配置成功 则可进入下一步
示例:
URL : 63c27f47.r7.cpolar.top/wechat/chec…
Token :AASAdd
回调地址:63c27f47.r7.cpolar.top
7.2.1 进入授权页
用户点击授权之后,服务商服务器发起一个携带授权码code的Get请求,指向设置的回调地址(../wechat/auth)
@Controller
@RequestMapping("/wechat")
public class WeChatController {
//登录二维码显示
@GetMapping("/login")
public void wxLogin(HttpServletResponse response) throws IOException {
response.setContentType("image/png");
QrCodeUtil.generate(WeChatUtil.getUrl(),400,400,"jpg",response.getOutputStream());
}
//回调接口
@GetMapping("/auth")
public Result callBack(String code, String state, HttpServletRequest request,
HttpServletResponse response, HttpSession session) throws IOException {
WeChatToken weChatToken = WeChatUtil.getToken(code);
System.out.println("token = " + weChatToken.getAccessToken() +"\n" +"getOpenId = " + weChatToken.getOpenid());
return weChatToken.getErrCode() == 40029?
new Result("fail", weChatToken.getErrMsg()):
new Result("success", weChatToken);
}
}
7.2.3 code换token
通过code换取token
//回调接口
@GetMapping("/auth")
public Result callBack(String code, String state, HttpServletRequest request,
HttpServletResponse response, HttpSession session) throws IOException {
WeChatToken weChatToken = WeChatUtil.getToken(code);
System.out.println("token = " + weChatToken.getAccessToken() +"\n" +"getOpenId = " + weChatToken.getOpenid());
return weChatToken.getErrCode() == 40029?
new Result("fail", weChatToken.getErrMsg()):
new Result("success", weChatToken);
}
7.2.4 token换用户数据
@GetMapping("/info")
public Result getInfo(String token,String openId) throws IOException {
WeChatUser user = WeChatUtil.getInfo(token, openId);
System.out.println("user = " + user);
return user.getErrCode()==40003?
new Result("fail","获取失败"):
new Result("success",user);
}
7.2.5 token刷新
@GetMapping("/refresh")
public Result refresh(String refreshToken) throws IOException {
WeChatToken weChatToken = WeChatUtil.refreshToken(refreshToken);
return weChatToken.getErrCode()==40029?
new Result("fail","获取失败"):
new Result("success", weChatToken);
}
7.2.6 完整代码
Result
package com.example.domain;
import lombok.Data;
@Data
public class Result {
private String status;//请求状态
private Object data;//数据
private String msg;//信息
public Result() {
}
public Result(String status, String msg) {
this.status = status;
this.msg = msg;
}
public Result(String status, Object data) {
this.status = status;
this.data = data;
}
}
WeChatToken
package com.example.domain;
import lombok.Data;
@Data
public class WeChatToken {
private String accessToken;//网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
private String expiresIn;//access_token接口调用凭证超时时间
private String refreshToken;//用户刷新access_token
private String openid;//用户唯一标识
private String scope;//用户授权的作用域
private String isSnapshotUser;//是否为快照页模式虚拟账号
private String unionId;//用户统一标识(针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的),只有当scope为"snsapi_userinfo"时返回
private Integer errCode;//错误码
private String errMsg;//错误信息
}
WeChatUser
package com.example.domain;
import lombok.Data;
@Data
public class WeChatUser {
private String openid;//用户唯一id
private String nickname;//微信昵称
private Integer sex;//性别
private String language;//语言
private String city;//城市
private String province;//省份
private String country;//国家
private String headImgUrl;//头像url
private String[] privilege;//特权
private String unionID;//用户综合id
private Integer errCode;//错误码
private String errMsg;//错误信息
}
WechatUtil
其中:RedirectUri是使用cpolar内网穿透形成的一个外网地址,微信无法验证localhost地址,请在微信开放平台和此次将域名和URL修改为自己的服务器指定地址或者内网穿透地址
public class WeChatUtil {
//常量区
public static final String AppId = "wxxx1234567";
public static final String AppSecret = "1122344555566678";
public static final String RedirectUri = "https://http://63c27f47.r7.cpolar.top//wechat/auth";63c27f47.r7.cpolar.top
//RedirectUri是使用cpolar内网穿透形成的一个外网地址,微信无法验证本地地址,请在微信开放平台和此次将域名和URL修改为自己的服务器指定地址或者内网穿透地址
//http请求客户端
static CloseableHttpClient httpClient = HttpClientBuilder.create().build();
//方法区
/**
* Get encoded WeChat Authorization Url which embedded AppID and so on
* @return completed url
*/
public static String getUrl(){
//url要转为 UrlEncode编码格式
String CodedRedirectUri = URLEncoder.encode(RedirectUri, StandardCharsets.UTF_8);
return "https://open.weixin.qq.com/connect/oauth2/authorize?" +
"appid="+AppId+
"&redirect_uri="+CodedRedirectUri+
"&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect&forcePopup=true";
}
/**
* Get token object by code
* @param code 授权码
* @return 返回 WeChatToken 对象
* @throws IOException IO异常
*/
public static WeChatToken getToken(String code) throws IOException {
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?"+
"appid="+AppId+
"&secret="+AppSecret+
"&code="+code+
"&grant_type=authorization_code";
//get请求对象装入url
HttpGet httpGet = new HttpGet(tokenUrl);
String responseResult = "";
//发起请求 如果请求成功 则接收返回的数据转为UTF-8格式
HttpResponse response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode()==200) {
responseResult = EntityUtils.toString(response.getEntity(),"UTF-8");
}
//将结果封装到WechatToken对象 并返回
return JSON.parseObject(responseResult, WeChatToken.class);
}
/**
* Refresh token
* @param token:not accessToken,this is refresh token
* @return new Token Object
* @throws IOException IO异常
*/
public static WeChatToken refreshToken(String token) throws IOException {
String url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?" +
"appid="+AppId+
"&grant_type=refresh_token" +
"&refresh_token="+token;
HttpGet httpGet =new HttpGet(url);
//执行刷新
HttpResponse response = httpClient.execute(httpGet);
String jsonStr = "";
if (response.getStatusLine().getStatusCode()==200) {
jsonStr = EntityUtils.toString(response.getEntity());
}
return JSON.parseObject(jsonStr, WeChatToken.class);
}
/**
* Get user information by access_token and openID
* @param accessToken 用户访问token
* @param openId 用户唯一id
* @return 微信用户对象
* @throws IOException IO异常
*/
public static WeChatUser getInfo(String accessToken,String openId) throws IOException{
String url = "https://api.weixin.qq.com/sns/userinfo?" +
"access_token="+accessToken+
"&openid=" +openId+
"&lang=zh_CN";
HttpGet httpGet = new HttpGet(url);
String jsonStr = "";
//执行请求
CloseableHttpResponse response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode()==200) {
jsonStr = EntityUtils.toString(response.getEntity());
}
System.out.println("jsonStr = " + jsonStr);
return JSON.parseObject(jsonStr,WeChatUser.class);
}
}
WeChatController
@RestController
@RequestMapping("/wechat")
public class WeChatController {
/**
* check the link status to WeChat Public Platform
* @param signature signature
* @param timeStamp timeStamp
* @param nonce nonce
* @param echoStr echoStr
* @return the same echoStr
*/
@GetMapping("/check")
public String WXCheck(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timeStamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echoStr){
return echoStr;
}
/**
* Generate QRCode jumped to WeChat Authorization website
* @param response 返回图片流
* @throws IOException IO异常
*/
@GetMapping("/login")
public void wxLogin(HttpServletResponse response) throws IOException {
response.setContentType("image/png");
QrCodeUtil.generate(WeChatUtil.getUrl(),400,400,"jpg",response.getOutputStream());
}
/**
* Authorize code to get token
* @param code code
* @param state state
* @param request request
* @param response response
* @param session session
* @return result
* @throws IOException io
*/
@GetMapping("/auth")
public Result callBack(String code, String state, HttpServletRequest request,
HttpServletResponse response, HttpSession session) throws IOException {
WeChatToken weChatToken = WeChatUtil.getToken(code);
System.out.println("token = " + weChatToken.getAccessToken() +"\n" +"getOpenId = " + weChatToken.getOpenid());
return weChatToken.getErrCode() == 40029?
new Result("fail", weChatToken.getErrMsg()):
new Result("success", weChatToken);
}
/**
* Refresh token
* @param refreshToken refreshToken
* @return new token
* @throws IOException io
*/
@GetMapping("/refresh")
public Result refresh(String refreshToken) throws IOException {
WeChatToken weChatToken = WeChatUtil.refreshToken(refreshToken);
return weChatToken.getErrCode()==40029?
new Result("fail","获取失败"):
new Result("success", weChatToken);
}
/**
* Get user information
* @param token token
* @param openId openID
* @return user info
* @throws IOException io
*/
@GetMapping("/info")
public Result getInfo(String token,String openId) throws IOException {
WeChatUser user = WeChatUtil.getInfo(token, openId);
System.out.println("user = " + user);
return user.getErrCode()==40003?
new Result("fail","获取失败"):
new Result("success",user);
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HELLO</title>
</head>
<body>
<h1>LOGIN</h1>
<a href="/wechat/login">微信登录</a>
</body>
</html>
8 OAuth2.0 - Gitee
8.1 准备工作
在Gitee官网登录后找到设置里面的第三方应用 创建第三方应用 - Gitee.com:
-
保存 ClientID 和 Client Secret
-
设置回调地址:Gitee和微信不一样,无需内网穿透,可以设置本地地址
http://127.0.0.1:8080/gitee/auth
-
主页地址作用不大,可随意填写
http://127.0.0.1:8080/page/main.html
-
权限:设置你的应用将要向用户索取的权限
-
其余参数请自定义
8.2 实现细节
和 微信的OAuth2.0授权大致相同
Result
@Data
public class Result {
private String status;//请求状态
private Object data;//数据
private String msg;//信息
public Result() {
}
public Result(String status, String msg) {
this.status = status;
this.msg = msg;
}
public Result(String status, Object data) {
this.status = status;
this.data = data;
}
}
GiteeUser
@Data
public class GiteeUser {
private String id;//id
private String name;//用户名
private String email;//邮箱
private String avatarUrl;//用户头像
private String url;//json-数据
private String htmlUrl;//json-用户主页
private String starredUrl;//json-用户收藏
private String blog;//用户博客连接
private String weibo;//绑定微博
private String createdAt;//账号创建日期
private Date updatedAt;//最近项目活跃时间
}
GiteeToken
@Data
public class GiteeToken {
private String accessToken;//token
private String tokenType;//token类型
private String expiresIn;//token过期时长 86400s = 1day
private String refreshToken;//刷新token
private String scope;//权限范围
private String createdAt;//token创建时间戳
private String error; //错误
private String errorDescription;//错误信息
}
GiteeUtil
public class GiteeUtil {
public static final String ClientID = "8e3exxxxxxxxxxxxxxxxxx";
public static final String ClientSecret = "88efc2c187exxxxxxxx";
public static final String RedirectUri = "http://127.0.0.1:8080/gitee/auth"; //回调地址
// public static final String RedirectUri = "https://2dbfb5d2.r7.cpolar.top/gitee/auth"; //回调地址
public static CloseableHttpClient httpClient = HttpClientBuilder.create().build();
/**
* 拼接 URL
* @return url
* @throws UnsupportedEncodingException Exception
*/
public static String getUrl() throws UnsupportedEncodingException {
String url = URLEncoder.encode(RedirectUri, StandardCharsets.UTF_8);
return "https://gitee.com/oauth/authorize?client_id=" + ClientID + "&redirect_uri=" + url + "&response_type=code";
}
/**
* 发起请求获取Token
* @param code 校验码
* @return 返回数据
* @throws Exception exception
*/
public static GiteeToken getToken(String code) throws Exception {
//新建httpClient对象 新建post请求对象
HttpPost postRequest = new HttpPost("https://gitee.com/oauth/token");
//post请求对象传入值
StringEntity input = new StringEntity(
"grant_type=authorization_code&" +
"code=" + code +
"&client_id=" + ClientID +
"&redirect_uri=" + RedirectUri +
"&client_secret=" + ClientSecret);
input.setContentType("application/x-www-form-urlencoded");
postRequest.setEntity(input);
//httpClient执行 post请求 并获取返回内容·
HttpResponse response = httpClient.execute(postRequest);
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(entity);
System.out.println("JSONSTR: "+jsonStr);
//控制台
GiteeToken gt = JSON.parseObject(jsonStr,GiteeToken.class);
return gt;
}
/**
* 刷新 Gitee 的 token
* @param refreshToken 先前获取到的refreshToken
* @return 返回新的 GiteeToken 对象
* @throws IOException exception
*/
public static GiteeToken RefreshToken(String refreshToken) throws IOException {
HttpPost postRequest = new HttpPost("https://gitee.com/oauth/token");
StringEntity input = new StringEntity("grant_type=refresh_token&refresh_token=" + refreshToken);
input.setContentType("application/x-www-form-urlencoded");
//数据传入方法体
postRequest.setEntity(input);
//执行请求
HttpResponse response = httpClient.execute(postRequest);
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(entity);
System.out.println(jsonStr);
//将JSON数据实例化为GiteeToken对象
return JSON.parseObject(jsonStr, GiteeToken.class);
}
/**
* 根据用户的token获取用户的信息
* @param token access_token
* @return W
* @throws IOException exception
*/
public static GiteeUser getInfo(String token) throws IOException {
//配置
HttpGet emailGet = new HttpGet("https://gitee.com/api/v5/emails?access_token="+token);
HttpGet userGet = new HttpGet("https://gitee.com/api/v5/user?access_token="+token);
//执行请求获取内容
HttpEntity entity1 = httpClient.execute(emailGet).getEntity();
HttpEntity entity2 = httpClient.execute(userGet).getEntity();
//获取邮箱
String jsonStr4email = EntityUtils.toString(entity1);
String substring = jsonStr4email.substring(1, jsonStr4email.length()-1);
String email = JSON.parseObject(substring).getString("email");
//获取用户
String jsonStr4User = EntityUtils.toString(entity2);
GiteeUser giteeUser = JSON.parseObject(jsonStr4User, GiteeUser.class);
giteeUser.setEmail(email);//将邮箱信息添加至用户
return giteeUser;
}
}
GiteeController
@Controller
@RequestMapping("/gitee")
public class GiteeController {
/**
* 拼接访问地址
* @return 跳转到拼接了clientID的url
*/
@GetMapping("/login")
public String giteeLogin() throws UnsupportedEncodingException {
return "redirect:"+GiteeUtil.getUrl();
}
/**
* Gitee 登录校验
* @param code 授权校验码
* @param session session
* @return res
* @throws Exception io
*/
@GetMapping("/auth")
@ResponseBody
public Result giteeAuth(@RequestParam("code") String code, HttpSession session) throws Exception {
GiteeToken giteeToken = GiteeUtil.getToken(code);
String token = giteeToken.getAccessToken();
System.out.println("giteeToken.toString() = " + giteeToken.toString());
return giteeToken.getError() ==null ?
new Result("success",giteeToken):
new Result("fail",giteeToken.getErrorDescription());
}
/**
* Refresh token
* @param refreshToken 先前 GiteeToken的refreshToken
* @return 新 GiteeToken对象
* @throws IOException io
*/
@GetMapping("refresh")
@ResponseBody
public Result refreshToken(String refreshToken) throws IOException {
GiteeToken newGiteeToken = GiteeUtil.RefreshToken(refreshToken);
//返回刷新情况
return newGiteeToken.getError()!=null?
new Result("fail",newGiteeToken.getErrorDescription()):
new Result("success",newGiteeToken);
}
/**
* Get gitee user information
* @param token token
* @return res
* @throws IOException io
*/
@GetMapping("/info")
@ResponseBody
public Result getInfo(String token) throws IOException {
return new Result("success",GiteeUtil.getInfo(token));
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HELLO</title>
</head>
<body>
<h1>LOGIN</h1>
<a href="/gitee/login">Gitee登录</a>
</body>
</html>