Spring Security Advanced Usage Sample

来源:互联网 发布:dating付费软件靠谱吗 编辑:程序博客网 时间:2024/04/20 13:13

需求

  1. 分布式微服务架构,使用Oauth2协议,Token类型使用Jwt(不是必须);
  2. 用户会在多个平台(PC,Android,IOS,微信)登录,同一个账号,不同平台可以同时登录,同一个平台只能登录一个实例
  3. 后登陆的实例可以在验证身份验证通过之后,将前面登录的实例踢掉;
  4. 退出会话功能,也就是让token失效;

设计

  1. jwt token的生成因素包括client+user+platform,也就是说获取token的流程除了要传递oauth2协议规定的client和user信息给授权服务器之外,还要在合适的时间点传递platform;
  2. 如何将前面的实例踢掉,我们知道jwt token是不用持久化的,他的信息都存储在token本身,也就是说只要发了token,授权服务器就控制不了这个token的后续生命周期了,如果要控制,就只能持久化,持久化之后,如果想把前面的实例踢掉,只需要用新的token覆盖掉之前的token就可以了;
  3. 如果想实现覆盖掉之前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@EnableOAuth2Clientpublic 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就可以了。

原创粉丝点击