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权限控制[四]

你可能感兴趣的:(Spring-Security-OAuth2资源服务器及SpringSecurity权限控制[四])