HGS的小站
a man that likes coding
首页
文章分类
按月归档
友情链接

前言

最近在做一个系统时,使用了 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()

效果展示

2022-09-15 |gs_huang | 笔记, python

为什么要写这篇文章?

之前也写过爬虫,用过python,也用过java,但对其原理一直是不求甚解,本着一种非要完全搞清楚的心态,对爬虫进行了更具体的学习,并写下此文章作为记录。

此文章以爬取微博热搜内容为例

需要哪些库?

requests

BeautifulSoup4

流程总览

  1. 获取网页html源码
  2. 按照标签规则查找指定(自己想要爬取的)数据
  3. 将数据存储到指定文件中

代码实现

获取网页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的修改值

  1. 在浏览器中进入网址:https://s.weibo.com/top/summary/(如需登录,需要先登录进去)
  2. 然后按F12,点击network(网络)
  3. 刷新一下网页
  4. 点击一个网络请求查看两个值,并复制粘贴到代码中

headers的参数查找

按照标签规则获取指定内容

先把网页内容用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')

爬取结果展示

爬取结果

2022-07-25 |gs_huang | 杂谈

将近两周的时间,终于拿下了备案。
此站总算是能够彻底开始运营~

2022-07-10 |gs_huang | 杂谈

今天开始,HGS小站正式开启~