Spring Security OAuth2.0认证受权六:先后端分离下的登陆受权

历史文章css

Spring Security OAuth2.0认证受权一:框架搭建和认证测试
Spring Security OAuth2.0认证受权二:搭建资源服务
Spring Security OAuth2.0认证受权三:使用JWT令牌
Spring Security OAuth2.0认证受权四:分布式系统认证受权
Spring Security OAuth2.0认证受权五:用户信息扩展到jwthtml

本篇文章将会解决上一篇文章《Spring Security OAuth2.0认证受权五:用户信息扩展到jwt 》中遗留的问题,并在原有的项目中新增模块business-server用来充当前端页面的web容器并转发登陆请求和更换token的请求等,以模拟先后端分离下的登陆以及更换token操做。前端

1、jwt令牌在网关处的过时时间校验

上一篇文章中讲了在网关处解析token并转发到目标服务的操做,由于使用了jwt令牌的缘由,因此省了一步到认证服务器认证的操做,只要验签成功,就认为令牌有效。这实际上留下了一个bug:服务端没法主动取消jwt令牌,因此这个令牌只要客户端保存下来,若是不调用认证服务器的令牌验证接口,这个jwt令牌将永远有效。所以须要在网关处加上对过时时间的校验。java

在TokenFilter中添加如下代码逻辑git

//取出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);
}

2、refresh-token接口缺乏用户信息

refresh-token在access_token过时,可是refresh-token未过时的时候使用,目的是使用refresh_token更新已通过期的access_token,这样理论上来讲,客户端只要能在refresh_token过时以前进行任意操做,就能够避免从新登陆了。web

上一篇文章中将用户信息放到了jwt token中并返回给客户端,可是若是使用refresh_token更新token,后端会报错,前端取到的token中则缺乏了用户信息。究其缘由,和JwtAccessTokenConverter有关系,关于这个类的实例,当初建立的方法以下spring

@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中。sql

通过debug,发现后端

2021-01-29_162556.jpg

最终发现是以下代码的问题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便可。

3、新建business-server模块做为web容器

这里新建的business-server模块有两个功能

  1. 充当web容器,该服务并无使用模板化技术,使用的是纯html、css实现前端
  2. 转发前端登陆、更换token请求

可能会有人对第二条有疑问,为何要这么作?以前测试的时候基本上都是使用postman发起的请求,请求的方式是这样的http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123能够看到这里传递了很重要的参数client_idclient_secret,这两个参数不管如何也不该当泄露给前端,一般都是中间的真正的客户端服务拼接这两个参数再将请求转发给认证服务

4、先后端分离

设计上想要实现如下功能

  1. 首页未登陆则提示用户登陆,已经登陆则展现用户我的信息
  2. 用户登陆以后将令牌保存到localStorage
  3. token过时以后用户能够选择使用refresh_token更换已通过期的令牌(access_token)
  4. 已通过期的refresh_token不能用于更换新的令牌

一、关闭认证服务表单登陆

之前请求认证服务的任意接口,若是没有认证,则都会跳转到系统自带的登陆页面,如今咱们想要实现先后端分离了,原来系统自带的登陆页面就有些碍眼了,直接关闭就好。关闭方法以下,spring security的配置更改成以下:

.formLogin()
                .disable();

二、先后端代码

前端代码在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

其它不作赘述,不过前端页面写起来挺麻烦的。。难是不难的

5、测试

源代码:

测试前首先须要从新执行初始化sql(auth-server/docs/sql/init.sql),而后依次启动 register-servergateway-serverauth-serverresource-serverbusiness-server 五个服务

启动成功后打开浏览器,输入http://127.0.0.1:30002/地址,就会看到如下页面
2021-01-29_171442.jpg
点击登陆以后,出现登陆框
2021-01-29_171532.jpg
输入帐号密码以后,登陆成功以后会跳转首页,就会看到我的信息
2021-01-29_171754.jpg
这里设置的token有效期为10秒,因此很快token就会失效,十秒钟以后刷新页面就会有新的提示
2021-01-29_171858.jpg
接下来能够有两种选择,一种是使用refresh-token更新失效的令牌,另一种是从新登陆,这里refresh_token的有效期也很短,只有30秒,若是超出30秒,则会更新失败,提示以下
2021-01-29_172049.jpg
而若是在30秒内刷新令牌,则会从新获取到令牌并刷新当前页

6、源代码地址

源代码地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0

个人博客地址:https://blog.kdyzm.cn/

相关文章
相关标签/搜索