将token存入redis,每次访问验证redis里的token对应的用户信息。判断是否密码变更,账号是否过期等信息。
这是我在网上能找到的的关于jwt的教程中看到最为广泛的一种做法,个人非常抗拒这种做法。
我们使用最原始的就是session模式,有状态模式,利用客户端与服务端的session缓存来达到辨别用户的目的。时至今日,我们仍然可以看到一些项目在使用。
之后是redis时代,有状态模式(为了解决分布式session共享应景而生的一种手段),利用redis存放session来达到不同实例之间的session共享。
最后是令牌模式,无状态模式(中间经历了多少其实我并不清楚,因为我其实除了学习阶段,上手便是JWT),也就是token模式,token模式的出现又为了解决什么样的问题呢。答案是服务器压力,用户量日益增加的web服务如果每建立一个会话就在服务端存放一条session缓存,无疑对服务器的压力是巨大的。而且session也不并不安全,当然这不是本文讨论的内容。
再来说说开篇为什么说个人抗拒使用redis来存放token的做法。其实已经显而易见了。之所以使用jwt,就是因为要解决服务器存放session带来的巨大服务器压力。这倒好,好不容易从有状态进化到无状态模式,一个redis,又给整回去了!
记住一件事情,使用Token模式(严谨的讲,是基于JWT的Token模式,实际上springcloud oauth2 原生token就是个session_id,仅此而已),首先第一点,禁用任何形式的session,否则,请用回session模式
题外话:单点登录也不要使用Nginx来把服务都转发到统一域名下,然后共享Cookies来实现。可不可行?可行,但是非常的low。因为如果把Nginx看成一个整体的服务。那么这种做法无疑是单点了个寂寞(下次有时间再深入讨论这个问题)。
我们都知道,oauth2认证模式,授权码模式,隐藏模式,密码模式,客户端模式,此外还有一种,刷新令牌模式,这并非单独的一种,而是以上4中都可以配合刷新token模式来使用。
而我们这里讲最严谨的授权码模式,采用自动授权方式(即:登陆后不用手动点击授权)。
思路:
已知:刷新令牌(refresh token)的作用,在token过期后,可以在不登陆的情况下使用刷新token到认证服务器获取新的token而无需再次登录。
得知:以短到令人发指的token失效时间来保证已颁发的令牌总是失效(5分钟,甚至更短)。在token失效时使用隐蔽的方式重新获得新的token,就可以达到一种修改密码之后,原来的token会在5分钟甚至更短时间之内失效。而如果没有密码变更,则一直可以访问。但是token5分钟变化一次用户却感知不到的一种效果。用户无感知,密码改了,token失效了。不就是我们想达到的效果吗。尽管它并不是实时的,但是至少看上去是像的。
客户端的配置
// 客户端配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_name")
.secret(passwordEncoder.encode("client_secret"))
// 授权码模式,刷新token模式
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("scopename")
// 是否自动授权
.autoApprove(true)
/*
* 可颁发授权码的服务地址,这里是瞎写的,正常开发环境应从数据库读取
* 这里应该是一个可以处理授权码的一个页面,记住是页面而非接口
* 任何授权相关的内容皆与资源服务器无关,只跟授权服务器做交互
* */
.redirectUris("http://localhost/token");
}
token颁发时的处理与刷新token时的处理
import com.hrx.entity.AuthUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.LinkedHashMap;
import java.util.Map;
public class MetroJwtTokenConverter extends JwtAccessTokenConverter {
@Autowired
UserDetailsService userDetailsService;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
AuthUser user;
// 这个判断可以判断是不是来刷新token的,AuthUser是我们自己的用户实体类。它应实现自UserDetails接口
if(authentication.getPrincipal() instanceof AuthUser) {
// 不是来刷新token的。可以这样拿到用户信息。
user = (AuthUser) authentication.getPrincipal();
}else {
// 是刷新token的,authentication.getPrincipal()只能获取到用户名,首先拿到刷新token
String tokenValue = authentication.getOAuth2Request().getRefreshTokenRequest().getRequestParameters().get("refresh_token");
// 从token中获取信息
Map<String, Object> decode = this.decode(tokenValue);
// authentication.getPrincipal()只能拿到用户名, 这里是关键代码,从数据库获取该用户,比对密码。当然这个密码是加密过的
user = (AuthUser) userDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
// 已发生修改或者已被封禁的逻辑都可以在这里判断,然后抛出这个异常。即:401异常
if(!user.getPassword().equals(decode.get("verification"))) {
throw new CredentialsExpiredException("密码已在其它地方修改, 登录失效");
}
}
// 我们要往token中存信息
Map<String, Object> additionalInformation = new LinkedHashMap<>();
additionalInformation.put("company_id", user.getCompanyNum());
additionalInformation.put("step", user.getStep());
/*
* 这里是关键的地方,其实就是用户密码。当然是加密后的。这里要存
* 上面才能获取到。这里会在颁发token的时候被执行到。所以token中会存在这些信息
*/
additionalInformation.put("verification", user.getPassword());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
return super.enhance(accessToken, authentication);
}
}
更短的token失效时间,与更长的刷新token失效时间
// 令牌访问端点与令牌服务
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
// .pathMapping()// 映射自定义访问路径
.userDetailsService(userDetailsService)
.authorizationCodeServices(authorizationCodeServices())
// .tokenStore(tokenStore)
// .tokenEnhancer(jwtAccessTokenConverter)
.tokenServices(tokenService(endpoints))
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
.pathMapping("/oauth/confirm_access", "/confirm_access");
}
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
private AuthorizationServerTokenServices tokenService(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true);//允许支持刷新token
defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 5分钟失效,如果发生密码修改等账户变更,300秒内旧token将失效
defaultTokenServices.setAccessTokenValiditySeconds(300);
// 如果没有变更,则这个刷新token可以在2天内获取到新的token
defaultTokenServices.setRefreshTokenValiditySeconds(2 * 24 * 60 * 60);
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
defaultTokenServices.setTokenStore(tokenStore);
return defaultTokenServices;
}
这里也是重头戏,关于vue前端对刷新token的支持,非常优雅的操作
把axios实例的操作都代理一便,把每个请求的then,catch,finally都跟对应的请求ID一起存在vuex里
import service from '@/utils/authorizationRequest'
import store from '@/store'
export default function Request(baseURL, config) {
// 每一个请求都有自己的ID
const conf = { ...config, requestId: ++ store.state.app.requestCurrentId }
// 创建一个请求
this.request = service(baseURL)(conf)
// 把请求存起来
store.state.app.requests[conf.requestId] = {}
// 代理原本的then操作, 为什么要代理,非常重要,这里传入一个函数
this.then = function(resolve) {
// then操作存起来
store.state.app.requests[conf.requestId].thenFunc = resolve
// 执行then操作
this.request.then(resolve)
// 连着点,它是真的香
return this
}
this.catch = function(reject) {
// catch操作也存起来
store.state.app.requests[conf.requestId].catchFunc = reject
this.request.catch(reject)
return this
}
this.finally = function(fl) {
// finally操作当然也存起来
store.state.app.requests[conf.requestId].finallyFunc = fl
this.request.finally(fl)
return this
}
}
拦截返回401的请求(JS真的是烦得很呢,我不知道括号有没有对齐,将就看下吧,反正也是片段,相信看到这里的没有谁是很菜的。应该都能明白我想表达的意思,甚至各位大神可以自行优化,有不对的地方也希望指出来,实现全民进化嘛)
import axios from 'axios'
import store from '@/store'
import refreshToken from '@/api/refreshToken'
// 不要每个401都去刷新一次,第一个401标价为刷新正在进行,则标记为true,刷新完了标记回来
let isRefresh = false
// 401的请求存在这儿
let requests = []
export default function service(baseURL) {
// 创建一个axios请求
const service = axios.create({ baseURL })service.interceptors.response.use(
response => {
// 已经成功的请求就不要浪费内存了。记得回收
store.state.app.requests[response.config.requestId] = undefined
//请求成功前置处理,省略吧,太多了
return res.data
},error => {
if (error.response.status === 401) {
/*
* 401的都给它存起来,这些是在token失效之后发送的请求,必定都会401
* 我们并不希望它就此没了
* 而是希望我们用了新的token之后,可以接它们们回来
* 逝去的先辈们终究不是真的逝去
* */
requests.push(error.config)
// 如果还没有开始刷新token,就从这里开始
if (!isRefresh) {
// 标记为正在刷新
isRefresh = true
console.log('开始刷新token')
// 这个请求不能从本axios配置获取,它不能有这个刷新token逻辑,因为它自己就是刷新token的
// 它失败了就是失败了,没救了
refreshToken()
.then(res => {
// 往cookie里写token
setToken(res.access_token)
// 遍历失效期间401的请求
requests.forEach(config => {
// 给它换个token
config.headers[TokenKey] = res.access_token
// 重新创建请求
service(config)
.then(res => {
// 看看vuex里存了这个请求的then操作没
if (store.state.app.requests[config.requestId].thenFunc) {
// 有的话再执行一次
store.state.app.requests[config.requestId].thenFunc(res)
}
})
.catch(res => {
// 看看catch方法
if (store.state.app.requests[config.requestId].catchFunc) {
store.state.app.requests[config.requestId].catchFunc(res)
}
})
.finally(() => {
// 再看看finally方法
if (store.state.app.requests[config.requestId].finallyFunc) {
store.state.app.requests[config.requestId].finallyFunc()
}
// 完了之后记得清理store里存放的东西
store.state.app.requests[config.requestId] = undefined
})
})
// 把存放的请求清理一下
requests = []
})
.catch(err => {
Message({
message: '无效令牌',
type: 'error',
duration: 5 * 1000
})
setTimeout(() => {
window.location.href = '/'
}, 2000)
})
.finally(() => {
console.log('刷新token完成, 重置刷新状态')
isRefresh = false
})
}
}