您的位置:首页 > 编程语言 > Java开发

Spring Security Advanced Usage Sample

2017-09-11 10:35 211 查看

需求

分布式微服务架构,使用Oauth2协议,Token类型使用Jwt(不是必须);

用户会在多个平台(PC,Android,IOS,微信)登录,同一个账号,不同平台可以同时登录,同一个平台只能登录一个实例

后登陆的实例可以在验证身份验证通过之后,将前面登录的实例踢掉;

退出会话功能,也就是让token失效;

设计

jwt token的生成因素包括client+user+platform,也就是说获取token的流程除了要传递oauth2协议规定的client和user信息给授权服务器之外,还要在合适的时间点传递platform;

如何将前面的实例踢掉,我们知道jwt token是不用持久化的,他的信息都存储在token本身,也就是说只要发了token,授权服务器就控制不了这个token的后续生命周期了,如果要控制,就只能持久化,持久化之后,如果想把前面的实例踢掉,只需要用新的token覆盖掉之前的token就可以了;

如果想实现覆盖掉之前token的功能,那我们在获取token的时候,还需要给授权服务器传递是否强制生成access_token的标识recreate_access_token;

实现思路

根据上边的要求,我们只需要继承JwtTokenStore,重载里面的关于OAuth2AccessToken、OAuth2Authentication和OAuth2RefreshToken持久化的相关实现,并增加platform和recreate_access_token的处理逻辑。

其实就是将Spring Security OAuth2中已经提供的RedisTokenStore和JwtTokenStore的功能整理起来;

如果想存储在关系型数据库里的话,就是将JdbcTokenStore和JwtTokenStore的功能整合起来

具体实现

[PanJwtTokenStore]

/**
*
* @author chenzhenyang
*
*/
public class PanJwtTokenStore extends JwtTokenStore {

private JwtAccessTokenConverter jwtTokenEnhancer;

private static final String CACHE_NAME = "JWT_TOKEN_STORE";

private static final String KEY_ACCESS_TOKEN_PREFIX = "JWT_ACCESS_TOKEN";
private static final String KEY_REFRESH_TOKEN_PREFIX = "JWT_REFRESH_TOKEN";

private static final String RECREATE_ACCESS_TOEKN_KEY = "recreate_access_token";
private static final String PLATFORM_KEY = "platform";

private Cache cache;

public PanJwtTokenStore(JwtAccessTokenConverter jwtTokenEnhancer, CacheManager cacheManager) {
super(jwtTokenEnhancer);
this.jwtTokenEnhancer = jwtTokenEnhancer;
this.cache = cacheManager.getCache(CACHE_NAME);
}

@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String accessTokenCacheKey = getAccessTokenCacheKeyFromAuthentication(authentication);
ValueWrapper value = cache.get(accessTokenCacheKey);

@SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
String recreateAccessTokenStr = (String) details.getOrDefault(RECREATE_ACCESS_TOEKN_KEY, "true");
Boolean recreateAccessToken = Boolean.valueOf(recreateAccessTokenStr);

if (recreateAccessToken) {
return null;
}

if (StringUtils.isEmpty(value)) {
return null;
}

String tokenValue = (String) value.get();
DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) convertAccessToken(tokenValue);

String refreshTokenCacheKey = getRefreshTokenCacheKeyFromAuthentication(authentication);

OAuth2RefreshToken refreshToken =(OAuth2RefreshToken)cache.get(refreshTokenCacheKey).get();
accessToken.setRefreshToken(refreshToken);

return accessToken;
}

@Override
public OAuth2Authentication readAuthentication(String token) {
Map<String,Object> map = jwtTokenEnhancer.decode(token);
OAuth2Authentication authentication = jwtTokenEnhancer.extractAuthentication(map);
@SuppressWarnings("unchecked")
Map<String,Object> details = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
if(null == details){
details = new HashMap<>();
}
details.put("recreate_access_token", map.get("recreate_access_token"));
details.put("platform", map.get("platform"));
AbstractAuthenticationToken authenticationToken = (AbstractAuthenticationToken) authentication.getUserAuthentication();
authenticationToken.setDetails(details);
String key = getAccessTokenCacheKeyFromAuthentication(authentication);
ValueWrapper vw = this.cache.get(key);

if(ObjectUtils.isEmpty(vw)){
return null;
}

if(!token.equals(vw.get())){
return null;
}

//
//      String tokenValue = vw.get();
return authentication;
}

/**
*
* @param tokenValue
* @return
*/
private OAuth2AccessToken convertAccessToken(String tokenValue) {
Map<String, Object> map = jwtTokenEnhancer.decode(tokenValue);
return jwtTokenEnhancer.extractAccessToken(tokenValue, map);
}

private String getAccessTokenCacheKeyFromAuthentication(OAuth2Authentication authentication) {
String accessTokenCacheKey = KEY_ACCESS_TOKEN_PREFIX + getNoPrefixCacheKeyFromAuthentication(authentication);
return accessTokenCacheKey;
}

private String getAccessTokenCacheKeyFromAccessToken(OAuth2AccessToken accessToken) {
String accessTokenCacheKey = KEY_ACCESS_TOKEN_PREFIX + getNoPrefixCacheKeyFromAccessToken(accessToken);
return accessTokenCacheKey;
}

private String getRefreshTokenCacheKeyFromAuthentication(OAuth2Authentication authentication) {
String accessTokenCacheKey = KEY_REFRESH_TOKEN_PREFIX + getNoPrefixCacheKeyFromAuthentication(authentication);
return accessTokenCacheKey;
}

/**
*
* @param authentication
* @return
*/
private String getNoPrefixCacheKeyFromAuthentication(OAuth2Authentication authentication) {
@SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
String platform = (String) details.getOrDefault(PLATFORM_KEY, "pc");
String user = null;
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
user = principal.toString();
} else if (principal instanceof User) {
user = ((User) principal).getUsername();
}
return user + platform;
}

/**
*
* @param accessToken
* @return
*/
private String getNoPrefixCacheKeyFromAccessToken(OAuth2AccessToken accessToken) {
Map<String, Object> details = accessToken.getAdditionalInformation();
String platform = (String) details.get("platform");
if (StringUtils.isEmpty(platform)) {
platform = "pc";
}
Map<String, Object> additionalInformation = accessToken.getAdditionalInformation();
String user = (String) additionalInformation.get("user_name");
return user + platform;
}

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
String key = getAccessTokenCacheKeyFromAuthentication(authentication);
if (StringUtils.isEmpty(key)) {
key = UUID.randomUUID().toString();
}
String tokenValue = token.getValue();
cache.put(key, tokenValue);
}

/**
* 这个方法都是在判断accessToken过期的时候删除。
*/
@Override
public void removeAccessToken(OAuth2AccessToken token) {
super.removeAccessToken(token);
String key = getAccessTokenCacheKeyFromAccessToken(token);
this.cache.evict(key);
}

/**
* 一个accessToken,顶多有一个refreshToken。<br>
* key = KEY_PREFIX + user + platform
*/
@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
super.storeRefreshToken(refreshToken, authentication);
String key = getRefreshTokenCacheKeyFromAuthentication(authentication);
this.cache.put(key, refreshToken);
this.cache.put(refreshToken.getValue(), key);
}

/**
*
*/
@Override
public void removeRefreshToken(OAuth2RefreshToken token) {
super.removeRefreshToken(token);
ValueWrapper vw = this.cache.get(token.getValue());
if (ObjectUtils.isEmpty(vw)) {
return;
}
String key = (String) vw.get();
this.cache.evict(token.getValue());
this.cache.evict(key);
}

@Override
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
super.removeAccessTokenUsingRefreshToken(refreshToken);
}

}


[PanJwtAccessTokenConverter]

public class PanJwtAccessTokenConverter extends JwtAccessTokenConverter{

@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
accessToken = super.enhance(accessToken, authentication);
Map<String,Object> additionalInformation = accessToken.getAdditionalInformation();
@SuppressWarnings("unchecked")
Map<String,Object> details = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
String platform = (String) details.getOrDefault("platform","pc");
additionalInformation.put("platform", platform);
String recreateAccessTokenStr = (String)details.getOrDefault("recreate_access_token","false");
Boolean recreateAccessToken = Boolean.valueOf(recreateAccessTokenStr);
additionalInformation.put("recreate_access_token", recreateAccessToken);
((DefaultOAuth2AccessToken)accessToken).setValue(encode(accessToken, authentication));
return accessToken;
}
}


在ResourceServerConfigurerAdapter中配置一下:

@Configuration
@EnableResourceServer
@EnableOAuth2Client
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter{

private static final String DEMO_RESOURCE_ID = "resource1";

@Autowired
private CacheManager cacheManager;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Bean
public TokenStore jwtTokenStore() {
return new PanJwtTokenStore(jwtAccessTokenConverter,cacheManager);
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
resources.tokenStore(jwtTokenStore());
}

@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.requestMatchers().anyRequest()
.and().httpBasic().disable()
.anonymous().disable()
.authorizeRequests()
//                .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
.antMatchers("/**").authenticated();//配置order访问控制,必须认证过后才可以访问
// @formatter:on
}

}


总结

如果我们不使用JWT Token而是普通的token的话,可以模仿JdbcTokenStore进行扩展,只需要在oauth_access_token表中添加,platform和recreate_access_token这两个字段,然后重写JdbcTokenStore,将所有关于此表的增删改查都加上platform和recreate_access_token就可以了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: