遇到的问题
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禁止访问提示。
源代码地址
诸位看官,小弟技术有限,如上述有误,请指出!
网友评论