spring security 兼容表單和json請(qǐng)求+跨域+rememberme redis存儲(chǔ) 配置

image.png

image.png

跨域配置

 http.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class);

跨域filter類

@Component
public class CorsFilter implements Filter {

    private final static Logger log = LoggerFactory.getLogger(CorsFilter.class);


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //servletRequest.setCharacterEncoding("UTF-8");

        HttpServletResponse res = (HttpServletResponse) servletResponse;

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        Map<String, String[]> parameterMap = request.getParameterMap();

        log.info(request.getMethod() + ">>>>>>>>>>>>>>>" + JSONObject.toJSONString(parameterMap));


        String origin = request.getHeader("Origin");
        log.info("請(qǐng)求origin:" + origin);
        res.setHeader("Access-Control-Allow-Origin", "*");

        res.setContentType("text/html;charset=UTF-8");

        res.setHeader("Access-Control-Allow-Methods", "*");

        res.setHeader("Access-Control-Max-Age", "86400");

        res.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");

        res.setHeader("Access-Control-Allow-Credentials", "true");

        res.setHeader("XDomainRequestAllowed", "1");

        filterChain.doFilter(servletRequest, servletResponse);

    }
}

自定義UserAuthenticationFilter 繼承 UsernamePasswordAuthenticationFilter 替換掉原來的 UsernamePasswordAuthenticationFilter

 http.addFilterAt(UserAuthenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);

UserAuthenticationFilter 類

public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();

    @Override
    protected String obtainPassword(HttpServletRequest request) {
        String password = this.getBodyParams(request).get(super.getPasswordParameter());

        if(!StringUtils.isEmpty(password)){
            return password;
        }
        return super.obtainPassword(request);
    }

    @Override
    protected String obtainUsername(HttpServletRequest request) {
        String username = this.getBodyParams(request).get(super.getUsernameParameter());
        if(!StringUtils.isEmpty(username)){
            return username;
        }
        return super.obtainUsername(request);
    }

    /**
     * 獲取body參數(shù)  body中的參數(shù)只能獲取一次
     * @param request
     * @return
     */
    private Map<String,String> getBodyParams(HttpServletRequest request){
        Map<String,String> bodyParams =  threadLocal.get();
        if(bodyParams==null) {
            ObjectMapper objectMapper = new ObjectMapper();
            try (InputStream is = request.getInputStream()) {
                bodyParams = objectMapper.readValue(is, Map.class);
            } catch (IOException e) {
            }
            if(bodyParams==null) bodyParams = new HashMap<>();
            threadLocal.set(bodyParams);
        }

        return bodyParams;
    }
}

創(chuàng)建UserAuthenticationFilter

 private UserAuthenticationFilter UserAuthenticationFilterBean() throws Exception {
        UserAuthenticationFilter userAuthenticationFilter = new UserAuthenticationFilter();
        userAuthenticationFilter.setAuthenticationManager(super.authenticationManager());
        userAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        userAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        userAuthenticationFilter.setUsernameParameter("username");
        userAuthenticationFilter.setPasswordParameter("password");
        userAuthenticationFilter.setRememberMeServices(rememberMeServices());
        return userAuthenticationFilter;
    }

到這里遇到一個(gè)坑

后面想要加入rememberme 功能 按照正常的配置方法是不行的
因?yàn)閁sernamePasswordAuthenticationFilter已經(jīng)被我們替換掉了
所以只能自己去配置userAuthenticationFilter的RememberMeServices

創(chuàng)建remembermeservice

@Bean
    public RememberMeServices rememberMeServices() {
        PersistentTokenBasedRememberMeServices rememberMeServices = new PersistentTokenBasedRememberMeServices(REMEMBER_ME, myUserDetailsService, myRedisTokenRepository);
        rememberMeServices.setParameter(REMEMBER_ME);
        rememberMeServices.setTokenValiditySeconds(3600);
        return rememberMeServices;
    }

redis存儲(chǔ)實(shí)現(xiàn)類

@Component
public class MyRedisTokenRepository implements PersistentTokenRepository {
    private final static long TOKEN_VALID_DAYS = 30;

    private final static Logger log = LoggerFactory.getLogger(MyRedisTokenRepository.class);
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void createNewToken(PersistentRememberMeToken token) {
        if (log.isDebugEnabled()) {
            log.debug("token create seriesId: [{}]", token.getSeries());
        }
        String key = generateKey(token.getSeries());
        HashMap<String, String> map = new HashMap();
        map.put("username", token.getUsername());
        map.put("tokenValue", token.getTokenValue());
        map.put("date", String.valueOf(token.getDate().getTime()));
        redisTemplate.opsForHash().putAll(key, map);
        redisTemplate.expire(key, TOKEN_VALID_DAYS, TimeUnit.DAYS);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        String key = generateKey(series);
        HashMap<String, String> map = new HashMap();
        map.put("tokenValue", tokenValue);
        map.put("date", String.valueOf(lastUsed.getTime()));
        redisTemplate.opsForHash().putAll(key, map);
        redisTemplate.expire(key, TOKEN_VALID_DAYS, TimeUnit.DAYS);
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
        String key = generateKey(seriesId);
        List<String> hashKeys = new ArrayList<>();
        hashKeys.add("username");
        hashKeys.add("tokenValue");
        hashKeys.add("date");
        List<String> hashValues = redisTemplate.opsForHash().multiGet(key, hashKeys);
        String username = hashValues.get(0);
        String tokenValue = hashValues.get(1);
        String date = hashValues.get(2);
        if (null == username || null == tokenValue || null == date) {
            return null;
        }
        Long timestamp = Long.valueOf(date);
        Date time = new Date(timestamp);
        PersistentRememberMeToken token = new PersistentRememberMeToken(username, seriesId, tokenValue, time);
        return token;
    }

    @Override
    public void removeUserTokens(String username) {
        if (log.isDebugEnabled()) {
            log.debug("token remove username: [{}]", username);
        }
        byte[] hashKey = redisTemplate.getHashKeySerializer().serialize("username");
        RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
        try (Cursor<byte[]> cursor = redisConnection.scan(ScanOptions.scanOptions().match(generateKey("*")).count(1024).build())) {
            while (cursor.hasNext()) {
                byte[] key = cursor.next();
                byte[] hashValue = redisConnection.hGet(key, hashKey);
                String storeName = (String) redisTemplate.getHashValueSerializer().deserialize(hashValue);
                if (username.equals(storeName)) {
                    redisConnection.expire(key, 0L);
                    return;
                }
            }
        } catch (IOException ex) {
            log.warn("token remove exception", ex);
        }
    }

    /**
     * 生成key
     *
     * @param series
     * @return
     */
    private String generateKey(String series) {
        return "spring:security:rememberMe:token:" + series;
    }
}

到這里還是無法成功實(shí)現(xiàn)rememberme
繼續(xù)debug

發(fā)現(xiàn)驗(yàn)證cookie的時(shí)候使用的是
TokenBasedRememberMeServices
由于我們想要保存redis 在UserAuthenticationFilter中使用的是
PersistentTokenBasedRememberMeServices
生成cookie和驗(yàn)證的方法不一致

還需要加上配置

  http
                        .rememberMe()
                        .rememberMeServices(rememberMeServices());

現(xiàn)在表單登錄可以正常使用rememberme了俯逾,
但是application/json類型的登錄請(qǐng)求還是沒有返回rememberme 的cookie

繼續(xù)debug
AbstractRememberMeServices中的 rememberMeRequested 方法使用
request.getParameter(parameter); 獲取的是表單參數(shù)
現(xiàn)在只能重寫這個(gè)方法了

MyPersistentTokenBasedRememberMeServices

public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {

 
    public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }

    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        boolean b = super.rememberMeRequested(request, parameter);
        if (!b) {
            String rememberMe = ApplicationJsonContextHolder.getString(parameter);
            if (rememberMe != null &&
                    (rememberMe.equalsIgnoreCase("true") ||
                            rememberMe.equalsIgnoreCase("on") ||
                            rememberMe.equalsIgnoreCase("yes") ||
                            rememberMe.equals("1"))) {
                return true;
            }
        }
        return b;
    }
}

把配置類中的remembermeservice替換了

  @Bean
    public RememberMeServices rememberMeServices() {
        MyPersistentTokenBasedRememberMeServices rememberMeServices = new MyPersistentTokenBasedRememberMeServices(REMEMBER_ME, myUserDetailsService, myRedisTokenRepository);
        rememberMeServices.setParameter(REMEMBER_ME);
        rememberMeServices.setTokenValiditySeconds(3600);
        return rememberMeServices;
    }

因?yàn)橐鉀Qapplication/json復(fù)用的問題昧港,
新建了一個(gè)ApplicationJsonContextHolder類 深寥,

ApplicationJsonContextHolder

public class ApplicationJsonContextHolder {

    private final static Logger log = LoggerFactory.getLogger(ApplicationJsonContextHolder.class);

    private static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<>();

    public static void reset() {
        threadLocal.remove();
    }

    /**
     * 獲取body參數(shù)  body中的參數(shù)只能獲取一次
     *
     * @param request
     * @return
     */
    public static void init(HttpServletRequest request) {
        Map<String, String> bodyParams = threadLocal.get();
        if (bodyParams == null) {
            ObjectMapper objectMapper = new ObjectMapper();
            try (InputStream is = request.getInputStream()) {
                bodyParams = objectMapper.readValue(is, Map.class);
            } catch (IOException e) {
            }
            if (bodyParams == null) bodyParams = new HashMap<>();
            threadLocal.set(bodyParams);
        }
        //log.info("json數(shù)據(jù)》》》》》》》》》》》》" + JSONObject.toJSONString(bodyParams));
    }


    public static Object get(String key) {
        return threadLocal.get().get(key);
    }

    public static String getString(String key) {
        return String.valueOf(get(key));
    }
}

在過濾器中初始化這個(gè)ApplicationJsonContextHolder

@Component
public class ApplicationJsonFilter implements Filter {
    private final static Logger log = LoggerFactory.getLogger(ApplicationJsonFilter.class);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        ApplicationJsonContextHolder.init((HttpServletRequest) servletRequest);
        filterChain.doFilter(servletRequest, servletResponse);
        /*用完要?jiǎng)h掉*/
        ApplicationJsonContextHolder.reset();
    }
}

配置過濾器

 http.addFilterBefore(applicationJsonFilter, UsernamePasswordAuthenticationFilter.class);

到這里應(yīng)該就能同時(shí)支持表單請(qǐng)求和application/json請(qǐng)求的rememberme功能了

用了 threadlocal request中的輸入流沒有內(nèi)容了
最后還是使用了裝飾器模式
http://www.reibang.com/p/58c1afb8e6af

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艰毒,一起剝皮案震驚了整個(gè)濱河市回官,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渠驼,老刑警劉巖夹姥,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洽洁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)菲嘴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汰翠,“玉大人龄坪,你說我怎么就攤上這事「椿剑” “怎么了健田?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佛纫。 經(jīng)常有香客問我妓局,道長,這世上最難降的妖魔是什么呈宇? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任好爬,我火速辦了婚禮,結(jié)果婚禮上甥啄,老公的妹妹穿的比我還像新娘存炮。我一直安慰自己,他們只是感情好蜈漓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布穆桂。 她就那樣靜靜地躺著,像睡著了一般融虽。 火紅的嫁衣襯著肌膚如雪享完。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天有额,我揣著相機(jī)與錄音般又,去河邊找鬼。 笑死谆吴,一個(gè)胖子當(dāng)著我的面吹牛倒源,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播句狼,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼笋熬,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了腻菇?” 一聲冷哼從身側(cè)響起胳螟,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤昔馋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后糖耸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秘遏,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年嘉竟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邦危。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡舍扰,死狀恐怖倦蚪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情边苹,我是刑警寧澤陵且,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站个束,受9級(jí)特大地震影響慕购,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茬底,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一沪悲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桩警,春花似錦可训、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烂叔,卻和暖如春谨胞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒜鸡。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工胯努, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逢防。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓叶沛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忘朝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子灰署,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355