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就可以了。相关文章推荐
- Usage sample of unix semaphore
- spring boot中使用security样例抛异常,Circular view path跳转失败
- Spring 5.0+Spring Boot+security+spring cloud oauth2+Redis整合详情,记录那些遇到的一些坑
- springboot+mybatis+SpringSecurity 实现用户角色数据库管理(一)
- spring security 权限验证
- spring-security配置和原理简介
- 传智播客-- 教育办公系统集成 spring-security 框架实现权限模块
- org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilter
- SpringBoot、MyBatis、Shiro框架renren-security
- 使用Spring-security3.0 框架
- SpringSecurity+SpringMVC +Mybatis3.0实现的web小框架
- Spring boot Security Disable security
- OpenJWeb平台Spring Security+CAS SSO的配置
- Selenium - WebDriver Advanced Usage
- SPRING IN ACTION 第4版笔记-第八章Advanced Spring MVC-003-Pizza例子的基本流程
- Spring Security Custom Login Form Annotation Example
- springMVC+mybatis+spring security<三>:使用数据库管理资源
- Spring Boot Security 整合
- SpringBoot(Security)
- Spring Boot + Spring Security 防止用户在多处同时登录(一个用户同时只能登录一次)及源码分析