Spring-Security-OAuth2资源服务器及SpringSecurity权限控制[四]

遇到的问题

1、UserDetailService is required!
2、使用RefreshToken时,在UserDetailService接口的public UserDetails loadUserByUsername(String username)方法中发现username为null值。
先谈怎么出现的?
业务场景需要,使用获取到的AccessToken中的RefreshToken去重新获取新的AccessToken对象,也就是说撤销旧的AccessToken值,创建新的AccessToken值。比如在单点登陆SSO的情况下,这个是必须的。
之前的Demo中,可以使用password和client两种模式获取到AccessToken值,当我使用refresh_token模式时却发现返回错误提示:UserDetailService is required!

问题1,解决方式:注入自定义的UserDetailService的对象,如下:

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception {
        endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore).userDetailsService(userDetailsService);
    }

解决这个之后,发现出现了问题2,经过debug发现,当生成AccessToken的时候,会将OAuth2Authentication认证对象序列化后存入缓存中,进行保存[以RefreshToken的值为key],当使用RefreshToken的时候,会先使用RefreshToken的值为key读取字节流并反序列化成为OAuth2Authentication对象。而问题就出现在这里,因为我的Member对象未实现序列化接口,存储的时候默认将其全部序列化成字节进入Redis中,而因为未实现序列化,所以在反序列化的时候,导致Member对象属性值均为null,进而导致外层的loadUserByUsername方法参数值为null。

问题2,解决方案:实现序列化接口即可


Redis缓存数据结构记录概要

获取AccessToken与刷新RefreshToken等操作都会在Redis中生成相关的数据,下面便是对相关数据结构进行简要介绍:

    private static final String ACCESS = "access:";
    1、access:${tokenValue}} 为key,存放 OAuth2AccessToken 对象
    
    private static final String AUTH_TO_ACCESS = "auth_to_access:";
    2、access:${USERNAME+CLIENT_ID+SCOPE}} 为key,存放 OAuth2AccessToken 对象
    
    private static final String AUTH = "auth:";
    3、 auth:${tokenValue} 为key,存放 OAuth2Authentication 对象
    
    private static final String REFRESH_AUTH = "refresh_auth:";
    4、 refresh_auth:${refreshTokenValue} 为key,存放 OAuth2Authentication  对象
    
    private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
    5、 access_to_refresh:${tokenValue} 为key,存放 OAuth2RefreshToken 对象的value属性值
    
    private static final String REFRESH = "refresh:";
    6、 refresh:${refreshTokenValue} 为key,存放 OAuth2RefreshToken 对象
    
    
    private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
    7、 refresh_to_access:${refreshTokenValue} 为key,存放 OAuth2AccessToken 对象的value属性值
    
    private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
    8、 refresh_to_access:${clientIdValue} 为key,存放 OAuth2AccessToken 对象
    
    private static final String UNAME_TO_ACCESS = "uname_to_access:";
    9、 refresh_to_access:${clientIdValue+":"+userNameValue} 为key,存放 OAuth2AccessToken 对象

个人理解

通过上面对于Redis缓存中的数据结构的分析,我们可以看出来,通过AccessToken和RefreshToken,我们可以针对自己的应用来做比较粗糙的权限控制,比如,通过一个AccessToken的value直接查询是否存在这个AccessToken对象是否存在,或者查询OAuthAuthentication对象是否存在,并与当前数据库中进行匹配等等,当然这只是对于权限做比较粗糙的工作,我这里也只是做一个简要的比喻。而对于权限的真正的控制,实际依赖于Spring-Security的权限注解,例如:@PreAuthorize("hasRole('ADMIN')")。在本公司中,实际做的权限控制,很尴尬,正如我前面章节所展示的只做了简单的查询校验,却并未做权限的精确控制。SpringSecurity实在是太庞大,学习成本昂贵。个人理解而言,SpringSecurityOAuth2适用于做开放平台,于本公司而言,可能是出于业务考虑(有不下三十个定制或者私有的APP连接服务器),为了便于统一管理,于是采用了OAuth2的形式,如果诸位有更好的方法,请赐教。

OAuth2结合SpringSecurity的权限控制

数据源的配置

@Configuration
public class DataStoreConfig {
    public static final String REDIS_CACHE_NAME = "redis_cache_name";//不为null即可
    public static final String REDIS_PREFIX = "redis_cache_prefix";//不为null即可
    public static final Long EXPIRE = 60 * 60L;//缓存有效时间

    /**
     * 配置用以存储用户认证信息的缓存
     */
    @Bean
    RedisCache redisCache(RedisTemplate redisTemplate) {
        RedisCache redisCache = new RedisCache(REDIS_CACHE_NAME, REDIS_PREFIX.getBytes(), redisTemplate, EXPIRE);
        return redisCache;
    }

    /**
     * 创建UserDetails存储服务的Bean:使用Redis作为缓存介质
     * UserDetails user = this.userCache.getUserFromCache(username)
     */
    @Bean
    public UserCache userCache(RedisCache redisCache) throws Exception {
        UserCache userCache = new SpringCacheBasedUserCache(redisCache);
        return userCache;
    }

    /**
     * 配置AccessToken的存储方式:此处使用Redis存储
     * Token的可选存储方式
     * 1、InMemoryTokenStore
     * 2、JdbcTokenStore
     * 3、JwtTokenStore
     * 4、RedisTokenStore
     * 5、JwkTokenStore
     */
    @Bean
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
        return new RedisTokenStore(redisConnectionFactory);
    }
}

拦截器的配置

public class Oauth2Interceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        String accessToken = request.getParameter("access_token");
        if (StringUtils.isEmpty(accessToken)) {
            return false;
        }
        TokenStore tokenStore = (TokenStore) ApplicationSupport.getBean("tokenStore");
        OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
        if (oAuth2Authentication == null) {
            return false;
        }
        SecurityContextHolder.getContext().setAuthentication(oAuth2Authentication);
        return true;
    }
}

资源权限的控制

@RestController
@RequestMapping("/api")
public class TestController {
    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping("/test")
    public String test() {
        return "success";
    }

    @PreAuthorize("hasRole('TEST')")
    @RequestMapping("/test2")
    public String test2() {
        return "success";
    }
}

1、在你的资源服务器中,使用注解@EnableResourceServer,代表你的服务是资源服务器。上篇文章,是基于自己公司所创建的资源服务器,并不算是真正意义上的资源服务器,只有使用了注解@EnableResourceServer才是真正的OAuth2的资源服务器,这样Spring的SecurityInterceptor才会对资源进行拦截并权限认证。
2、在你的资源服务器中,创建一个与认证授权服务器中配置相同的TokenStore的Bean对象,用来查询认证信息拦截器
3、创建一个OAuth2拦截器,并在拦截器中,拦截并获取请求中的AccessToken的value值,根据value获取认证信息,并将认证信息存入上下文中。
4、注解配置需要拦截的URL,如:@PreAuthorize("hasRole('ADMIN')")表示该接口需要ADMIN角色才能访问。否则将返回403禁止访问提示。

源代码地址

诸位看官,小弟技术有限,如上述有误,请指出!

Spring-Security-OAuth2服务器之搭建认证授权服务器[一]

Spring-Security-OAuth2服务器搭建之AccessToken的检测[二]

Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]

Spring-Security-OAuth2资源服务器及SpringSecurity权限控制[四]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容