前言
最近在做一个系统时,使用了 token 令牌来进行前后端交互的权限认证。
token 一般用于前端向后端发起请求时的权限认证。
用户登录自己的账号后,会得到一个 token,放在每次请求的请求头里面,后端拿它来做权限认证。
一般来说,每个 token 都会有一个过期时间,如果后端获得了一个过期的 token,会拒绝此次请求,并通知前端此用户登录已过期。
前端拿到后端发来的过期通知,最基础的做法是直接将前端页面重定向到登录页面,让用户重新登陆。
但是这种思路存在一个问题:当用户正在使用该系统的关键时期,突然 token 过期了,就会被迫重新登陆,很显然,无论是开发者还是使用者,都不会容忍这种情况的发生。
这时候,就需要开发者使用无痛刷新 token 的技术,让用户无感知的刷新 token,避免出现以上情况。
实现思路
前端登录时,后端准备两个 token,一个有效时间较短的作为认证 token(token),一个有效时间较长的作为刷新 token(refreshToken),返回给前端。
前端拿到两个 token 后,把它们都存储在 localStorage 中,其中 token 用于每次请求的携带,refreshToken用于 token 过期后对 token 进行刷新的认证。
总体思路如下:
- token 未过期时,可以正常请求。
- token 过期了,refreshToken 未过期,请求后端的刷新token接口对两个接口均进行刷新。
- token 和 refreshToken 均过期了,则用户必须重新登录。
代码实现
前端
前端使用axios的响应拦截器功能。
对每次请求的响应数据进行拦截,检查响应中的状态码,如果是 401(权限过期),则进行刷新尝试。如果不是 401,则直接放行。
下面是 axios 相应拦截实现的核心代码:
//axios实例创建
const service = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
})
//刷新token的请求函数
function refreshToken(refreshToken) {
return service({
url: '/user/refreshToken/' + refreshToken,
method: 'get'
})
}
//因axios请求为异步请求,故可能会出现同时多次刷新token的情况
//该变量相当于给刷新token上了个锁
let isRefreshing = false
service.interceptors.response.use(
resp => {
//判断状态码
//也要排除掉刷新token的请求
if (resp.data.code === 401 && !resp.config.url.includes('/user/refreshToken')) {
//先查询vuex中是否有refreshToken
//如果没有,则直接重定向到登录页面进行重新登录
if (!store.getters.refreshToken) {
router.push('/login')
return resp
//如果有,则先判断是否已经有过刷新请求,如果没有,进行刷新请求
} else {
if (!isRefreshing) {
isRefreshing = true
return refreshToken(store.getters.refreshToken).then(res => {
//通过该请求响应数据的状态码判断刷新token(refreshToken)是否过期
if (res.data.code === 401) {
router.push('/login')
isRefreshing = false
return res
}else{
//未过期时会得到两个新的token,此时将其持久化
store.dispatch('setToken', res.data.data.token)
store.dispatch('setRefreshToken', res.data.data.refreshToken)
resp.config.headers.token = res.data.data.token
if (resp.config.method === 'post'||resp.config.method === 'put') {
resp.config.data=JSON.parse(resp.config.data)
}
isRefreshing = false
//重新发起因token过期而未能成功实现的请求
return service.request(resp.config)
}
})
}
}
} else {
return resp
}
},
// 请求错误响应(和本文功能无关)
error => {
console.log(error)
return Promise.reject(error)
}
)
后端
配置TokenUtil类
后端对 TokenUtil 添加三个函数。
一个是 refreshTokenSign(User user) 用于登录时对刷新token(refreshToken)的注册。
public static String refreshTokenSign(User user){
String refreshToken;
Date refreshAt=new Date(System.currentTimeMillis()+REFRESH_TIME);
refreshToken= JWT.create()
.withIssuer("auth0")
.withClaim("phone",user.getPhone())
.withClaim("password",user.getPassword())
.withClaim("role",user.getRole())
.withExpiresAt(refreshAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
return refreshToken;
}
一个是 refreshTokenSign(String token) 用于刷新 token 时对 refreshToken 的刷新。
public static String refreshTokenSign(String token){
String refreshToken;
Date refreshAt=new Date(System.currentTimeMillis()+REFRESH_TIME);
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(token);
refreshToken= JWT.create()
.withIssuer("auth0")
.withClaim("phone",jwt.getClaim("phone").asString())
.withClaim("password",jwt.getClaim("password").asString())
.withClaim("role",jwt.getClaim("role").asString())
.withExpiresAt(refreshAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
return refreshToken;
}
最后一个是 refreshToken(String refreshToken) 用于通过 refreshToken 刷新 token。
public static String refreshToken(String refreshToken){
String token;
Date expiresAt=new Date(System.currentTimeMillis()+EXPIRE_TIME);
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(refreshToken);
token= JWT.create()
.withIssuer("auth0")
.withClaim("phone",jwt.getClaim("phone").asString())
.withClaim("password",jwt.getClaim("password").asString())
.withClaim("role",jwt.getClaim("role").asString())
.withExpiresAt(expiresAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
log.info("刷新token成功");
return token;
}
编写刷新token的接口
该类用于接受前端的 刷新 token 的 api 请求并提供相应响应。
@ApiOperation("刷新token")
@GetMapping("/refreshToken/{refreshToken}")
public Result refreshToken(@PathVariable("refreshToken")String refreshToken){
if(!TokenUtil.verify(refreshToken)){
return Result.error(401,"刷新token已过期");
}
String token = TokenUtil.refreshToken(refreshToken);
String newRefreshToken = TokenUtil.refreshTokenSign(token);
JSONObject jsonObject=new JSONObject();
jsonObject.put("token",token);
jsonObject.put("refreshToken",newRefreshToken);
return Result.success(jsonObject);
}
WebMvcConfig 对接口放行
主要是:excludePath.add("/user/refreshToken/**");
。
该函数指定了不被 token 拦截器拦截的请求。
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> excludePath=new ArrayList<>();
excludePath.add("/user/login/**");
excludePath.add("/user/register");
excludePath.add("/user/refreshToken/**");
excludePath.add("/doc.html");
excludePath.add("/swagger-ui.html");
excludePath.add("/swagger-resources/**");
excludePath.add("/v2/api-docs");
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
综上,无痛刷新 token 功能已经实现。
我做了什么?
我用python写了一个可以自动查询寝室电费并群发至室友邮箱的程序。
为什么要做?
我们学校寝室的电费往往只有在断电后才知道电费已经用完,这常常使人非常尴尬。尤其是正在洗澡的人和正在用台式机敲代码的人。
于是笔者就想着校方电费查询后台有没有提供接口。
非常遗憾的是,并未在网上查到本校后勤后台任何关于api的消息。
于是一个大胆的想法就诞生了,它不提供接口,我可以自己利用开发者工具找啊!
我们学校的电费信息是在微信公众号上,于是我在微信上进入电费查询界面后复制链接,放在浏览器网址栏,竟然成功了!
于是F12,刷新,找到了关键请求,再在在线http接口测试加上cookie和相应参数进行post测试,竟然通了!
既然行得通,那说干就干!
怎么做?
首先,需要从网上获取电费信息
其次,将电费信息自动群发至指定邮箱
代码实现
获取电费信息
这里运用了requests库,具体使用可参考:python调用第三方接口 - 临安剑客 - 博客园 (cnblogs.com)
import requests
import json
# 修改请求头
headers = {
'content-type': 'application/json',
'User-Agent': '***',
'Cookie': '***'
}
# 获取电费信息的接口
url = '***'
# post参数
params = {
'key1': 'value1',
'key2': 'value2',
'***': '***'
}
# 获取信息
res = requests.post(url, json=params, headers=headers)
# 信息转为json格式(方便获取数据)
elc_info = json.loads(res.text)
# 读取剩余电费
elc_surplus = elc_info['data']['surplusList'][0]['surplus']
群发给指定邮箱
这里用了PyEmail库,具体使用参考:python自动发送邮件 - 临安剑客 - 博客园 (cnblogs.com)
import smtplib
from email.mime.text import MIMEText
mail_host = 'smtp.163.com'
mail_user = '***'
mail_pass = '***'
sender = '***'
receivers = ['***','***']
message = MIMEText('当前电量剩余: '+str(elc_surplus)+'度', 'plain', 'utf-8')
message['Subject'] = '当前电量剩余'
message['From'] = sender
message['To'] = receivers[0]
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host, 25)
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(
sender, receivers, message.as_string())
smtpObj.quit()
代码总览
import requests
import json
import smtplib
from email.mime.text import MIMEText
# 获取剩余电量
# 修改请求头
headers = {
'content-type': 'application/json',
'User-Agent': '***',
'Cookie': '***'
}
# 获取电费信息的接口
url = '***'
# post参数
params = {
'key1': 'value1',
'key2': 'value2',
'***': '***'
}
# 获取信息
res = requests.post(url, json=params, headers=headers)
# 信息转为json格式(方便获取数据)
elc_info = json.loads(res.text)
# 读取剩余电费
elc_surplus = elc_info['data']['surplusList'][0]['surplus']
# 发送邮件
mail_host = 'smtp.163.com'
mail_user = '***'
mail_pass = '***'
sender = '***'
receivers = ['***','***']
message = MIMEText('当前电量剩余: '+str(elc_surplus)+'度', 'plain', 'utf-8')
message['Subject'] = '当前电量剩余'
message['From'] = sender
message['To'] = receivers[0]
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host, 25)
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(
sender, receivers, message.as_string())
smtpObj.quit()
效果展示
为什么要写这篇文章?
之前也写过爬虫,用过python,也用过java,但对其原理一直是不求甚解,本着一种非要完全搞清楚的心态,对爬虫进行了更具体的学习,并写下此文章作为记录。
此文章以爬取微博热搜内容为例
需要哪些库?
requests
- 作用:获取网页的html源码
- 参考:Python requests 模块 | 菜鸟教程 (runoob.com)
BeautifulSoup4
- 作用:按照标签规则查找网页内容
- 参考:Beautiful Soup 中文文档
流程总览
- 获取网页html源码
- 按照标签规则查找指定(自己想要爬取的)数据
- 将数据存储到指定文件中
代码实现
获取网页html源码
需要注意的是,获取网页源码时,访问的网页可能会需要登录认证或者验证User-Agent是否为浏览器,从而导致获取失败。这种情况就要修改请求头,来对访问网页进行“欺骗”。
不需要验证的网页
import requests
# 获取html源码
res = requests.get('https://www.baidu.com')
# 指定解析方式,一般有两种:
# 1、gbk
# 2、utf-8
res.encoding = 'utf-8'
# 打印到控制台
print(res.text)
需要验证的网页
本次示例,爬取微博热搜是需要验证的
import requests
# 修改请求头,对服务器进行“欺骗”
headers = {
# 此处的两个参数需要自己修改
'User-Agent': '***',
'Cookie': '***'
}
# 获取请求的session
session = requests.Session()
# 获取html源码
response = session.get('https://s.weibo.com/top/summary/', headers=headers)
response.encoding ='utf-8'
print(response.text)
关于User-Agent和Cookie的修改值
- 在浏览器中进入网址:https://s.weibo.com/top/summary/(如需登录,需要先登录进去)
- 然后按F12,点击network(网络)
- 刷新一下网页
- 点击一个网络请求查看两个值,并复制粘贴到代码中
按照标签规则获取指定内容
先把网页内容用beautifulsoup处理一下
from bs4 import BeautifulSoup
html_str = response.text
soup = BeautifulSoup(html_str, 'html.parser')
获取指定内容(热搜内容)
# 标题以及网址
url_titles = soup.select('#pl_top_realtimehot>table>tbody>tr>.td-02>a')
# 热度
hotness = soup.select('#pl_top_realtimehot>table>tbody>tr>.td-02>span')
# 列表,用于存储爬取信息
news = []
# 处理内容
for i in range(len(url_titles) - 1):
# 先把信息放到字典中
# 因为第一个是置顶内容,所以需要i+1来排除掉
hot_news = {'title': url_titles[i+1].get_text(), 'url': 'https://s.weibo.com' + url_titles[i+1]['href'],
'hotness': hotness[i].get_text()}
# 有些热度为空,不属于热榜,排除掉
if hot_news['hotness'] != ' ':
#需要的数据存在列表中
news.append(hot_news)
存储爬取内容
本例采用csv格式保存信息
f = open('./微博热搜榜.csv', 'w', encoding='utf-8')
for item in news:
f.write(item['title'] + ',' + item['url'] + ',' + item['hotness'] + '\n')
代码总览
import requests
from bs4 import BeautifulSoup
headers = {
'User-Agent': '***',
'Cookie': '***'
}
session = requests.Session()
response = session.get('https://s.weibo.com/top/summary/', headers=headers)
html_str = response.text
soup = BeautifulSoup(html_str, 'html.parser')
url_titles = soup.select('#pl_top_realtimehot>table>tbody>tr>.td-02>a')
hotness = soup.select('#pl_top_realtimehot>table>tbody>tr>.td-02>span')
news = []
for i in range(len(url_titles) - 1):
hot_news = {'title': url_titles[i+1].get_text(), 'url': 'https://s.weibo.com' + url_titles[i+1]['href'],
'hotness': hotness[i].get_text()}
if hot_news['hotness'] != ' ':
news.append(hot_news)
f = open('./微博热搜榜.csv', 'w', encoding='utf-8')
for item in news:
f.write(item['title'] + ',' + item['url'] + ',' + item['hotness'] + '\n')