历史文章
Spring Security OAuth2.0认证授权一:框架搭建和认证测试
Spring Security OAuth2.0认证授权二:搭建资源服务
Spring Security OAuth2.0认证授权三:使用JWT令牌
Spring Security OAuth2.0认证授权四:分布式系统认证授权
Spring Security OAuth2.0认证授权五:用户信息扩展到jwt
本篇文章将会解决上一篇文章《Spring Security OAuth2.0认证授权五:用户信息扩展到jwt 》中遗留的问题,并在原有的项目中新增模块business-server
用来充当前端页面的web容器并转发登录请求和更换token的请求等,以模拟前后端分离下的登录以及更换token操作。
一、jwt令牌在网关处的过期时间校验
上一篇文章中讲了在网关处解析token并转发到目标服务的操作,因为使用了jwt令牌的原因,所以省了一步到认证服务器认证的操作,只要验签成功,就认为令牌有效。这实际上留下了一个bug:服务端无法主动取消jwt令牌,所以这个令牌只要客户端保存下来,如果不调用认证服务器的令牌验证接口,这个jwt令牌将永远有效。因此需要在网关处加上对过期时间的校验。
在TokenFilter中添加以下代码逻辑
//取出exp字段,判断token是否已经过期
try {
Map<String, Object> map = objectMapper.readValue(payLoad, new TypeReference<Map<String, Object>>() {
});
long expiration = ((Integer) map.get("exp")) * 1000L;
if (expiration < new Date().getTime()) {
return unAuthorized(exchange, "未认证的请求:token存在,但是已经失效",WrapperResult.TOKEN_EXPIRE);
}
} catch (IOException e) {
log.error("", e);
return unAuthorized(exchange, "未认证的请求:错误的token",null);
}
二、refresh-token接口缺少用户信息
refresh-token在access_token过期,但是refresh-token未过期的时候使用,目的是使用refresh_token更新已经过期的access_token,这样理论上来说,客户端只要能在refresh_token过期之前进行任意操作,就可以避免重新登录了。
上一篇文章中将用户信息放到了jwt token中并返回给客户端,但是如果使用refresh_token更新token,后端会报错,前端取到的token中则缺少了用户信息。究其原因,和JwtAccessTokenConverter
有关系,关于这个类的实例,当初创建的方法如下
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//对称秘钥,资源服务器使用该秘钥来验证
return jwtAccessTokenConverter;
}
这里的new
操作省了很多默认参数的指定,且先看下为啥会缺少用户信息,扩展用户信息的关键在于方法com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername
,这里扩展了用户信息,使其从单纯的username字符串变成了UserDetailsExpand
对象,然后在增强方法com.kdyzm.spring.security.auth.center.enhancer.CustomTokenEnhancer#enhance
中将扩展信息取出来放到Token中。
经过debug,发现
最终发现是如下代码的问题org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter#extractAuthentication
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
//运行到这里的时候userDetailsService为空,所以并没有执行自定义的loadUserByUsername方法
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
层层网上追寻调用链,竟然是JwtAccessTokenConverter
创建的时候省略参数导致的,只需要如此做就可以解决问题了
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
userTokenConverter.setUserDetailsService(userDetailsService);
tokenConverter.setUserTokenConverter(userTokenConverter);
jwtAccessTokenConverter.setAccessTokenConverter(tokenConverter);
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//对称秘钥,资源服务器使用该秘钥来验证
return jwtAccessTokenConverter;
}
JwtAccessTokenConverter对象创建的时候指定DefaultUserAuthenticationConverter使用的userDetailsService即可。
三、新建business-server模块作为web容器
这里新建的business-server模块有两个功能
- 充当web容器,该服务并没有使用模板化技术,使用的是纯html、css实现前端
- 转发前端登录、更换token请求
可能会有人对第二条有疑问,为什么要这么做?之前测试的时候基本上都是使用postman发起的请求,请求的方式是这样的http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
可以看到这里传递了很重要的参数client_id
和client_secret
,这两个参数无论如何也不应当泄露给前端,通常都是中间的真正的客户端服务拼接这两个参数再将请求转发给认证服务
四、前后端分离
设计上想要实现以下功能
- 首页未登录则提示用户登录,已经登录则展示用户个人信息
- 用户登录之后将令牌保存到localStorage
- token过期之后用户可以选择使用refresh_token更换已经过期的令牌(access_token)
- 已经过期的refresh_token不能用于更换新的令牌
1、关闭认证服务表单登录
以前请求认证服务的任意接口,如果没有认证,则都会跳转到系统自带的登录页面,现在我们想要实现前后端分离了,原来系统自带的登录页面就有些碍眼了,直接关闭就好。关闭方法如下,spring security的配置更改为如下:
.formLogin()
.disable();
2、前后端代码
前端代码在business-server/src/main/resources/static
目录下,只有两个页面,一个首页,一个登陆页面
后端只有两个接口
- 登录接口:com.kdyzm.spring.security.oauth.study.business.server.controller.LoginController#login
- 更新token接口:com.kdyzm.spring.security.oauth.study.business.server.controller.TokenController#refreshToken
其它不做赘述,不过前端页面写起来挺麻烦的。。难是不难的
五、测试
源代码:
测试前首先需要重新执行初始化sql(auth-server/docs/sql/init.sql),然后依次启动 register-server
、gateway-server
、auth-server
、resource-server
、business-server
五个服务
启动成功后打开浏览器,输入http://127.0.0.1:30002/
地址,就会看到以下页面
点击登录之后,出现登录框
输入账号密码之后,登录成功之后会跳转首页,就会看到个人信息
这里设置的token有效期为10秒,所以很快token就会失效,十秒钟之后刷新页面就会有新的提示
接下来可以有两种选择,一种是使用refresh-token更新失效的令牌,另外一种是重新登录,这里refresh_token的有效期也很短,只有30秒,如果超出30秒,则会更新失败,提示如下
而如果在30秒内刷新令牌,则会重新获取到令牌并刷新当前页
六、源代码地址
源代码地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0
我的博客地址:https://blog.kdyzm.cn/