Spring Cloud+Spring boot Sercuity+JWT進(jìn)行用戶(hù)認(rèn)證及用戶(hù)信息在微服務(wù)間的傳遞

一琼娘、spring boot Security+JWT 在Spring Cloud網(wǎng)關(guān)層實(shí)現(xiàn)用戶(hù)認(rèn)證

1、引入Spring Security

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2、創(chuàng)建WebSecurityConfig繼承WebSecurityConfigurerAdapter
重寫(xiě)configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web應(yīng)用安全配置的適配器竟秫。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 由于使用的是JWT愕提,我們這里不需要csrf
                .csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()             
                // 對(duì)于獲取token的rest api要允許匿名訪問(wèn)
                .antMatchers("/api-user/safeVerify/**").permitAll()
                .antMatchers("/api-base/manage/userLogin/**").permitAll()
                .antMatchers("/api-user/app/login").permitAll()
                .antMatchers("/api-user/app/version").permitAll()
                .antMatchers("/api-user/app/refresh/token").permitAll()
                .antMatchers("/swagger-ui.html/**", "/swagger-resources/**", "/*/v2/api-docs/**").permitAll()//swagger文檔無(wú)授權(quán)訪問(wèn)

                // 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
                .anyRequest().authenticated();

        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

        // 禁用緩存
        httpSecurity.headers().cacheControl();
    }
}

Spring Security包含了眾多的過(guò)濾器,這些過(guò)濾器形成了一條鏈脖祈,所有請(qǐng)求都必須通過(guò)這些過(guò)濾器后才能成功訪問(wèn)到資源喻奥。其中UsernamePasswordAuthenticationFilter過(guò)濾器用于處理基于表單方式的登錄認(rèn)證过牙。我們通過(guò)該過(guò)濾器,實(shí)現(xiàn)JWT的用戶(hù)認(rèn)證。

3蚁袭、創(chuàng)建JWTAuthenticationTokenFilter 過(guò)濾器鬼悠,實(shí)現(xiàn)對(duì)請(qǐng)求token的用戶(hù)認(rèn)證

@SuppressWarnings("SpringJavaAutowiringInspection")
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private UserApi userApi;

    private static final String ERP_HEADER = "authorization-erp-fqkj";
     private static final String FILTER_APPLIED = "__spring_security_Filter_filterApplied";
    private static final String HEADER_USER = "key_userinfo_in_http_header";
    private static final String TOKEN_EXPRIED = "Filter_TokenExpried";


    @Override
    protected  void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        if (request.getMethod().equals("OPTIONS")) {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
            response.setHeader("Content-Type", "application/json");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Content-Type,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }        
        request.setAttribute(FILTER_APPLIED, true);
        AppendHeaderRequestWrapper requestWrapper = new AppendHeaderRequestWrapper(request);
        String authToken = request.getHeader(ERP_HEADER);       
        if (authToken != null) {
            String userSubject = jwtTokenUtil.getUserIdFromToken(authToken);
            boolean isExpired = jwtTokenUtil.isTokenExpired(authToken);
            if(!isExpired){
                setUserSecurityContext(userSubject, requestWrapper);
            }
        } 
        chain.doFilter(requestWrapper, response);
    }

    private void setUserSecurityContext(String userSubject, AppendHeaderRequestWrapper requestWrapper) {
        String companyId = userSubject.split("#")[0];
        String userId = userSubject.split("#")[1];
        String key = companyId + RedisSuffixConstants.LOGIN_USERLIST;
        boolean isExist = redisUtil.hHasKey(key, userId);
        UserInfo userInfo;
        if (isExist) {
            userInfo = redisUtil.hget(key, userId);
        } else {
            //如果redis中不存在
            userInfo = userApi.getUserInfoByUserId(userId);
        }
        if (userInfo == null) {
            return;
        }

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(userSubject);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(requestWrapper));
        SecurityContextHolder.getContext().setAuthentication(authentication);

        redisUtil.hset(key, userId, userInfo);
        UserInfoContext.setUser(userInfo);
        String userJson = JSON.toJSONString(userInfo);
        try {
            requestWrapper.putHeader(HEADER_USER, URLDecoder.decode(userJson, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            log.error("init userInfo error", e);
        }
    }    
}

該過(guò)濾器的處理步驟:
1)判斷httpmothod為“OPTIONS”,直接放通,允許跨域訪問(wèn)。
2)判斷請(qǐng)求header中是否存在指定名的token,調(diào)用JWT工具類(lèi)獲取當(dāng)前用戶(hù)標(biāo)識(shí)缎讼,判斷token是否過(guò)期笛辟。
3)根據(jù)用戶(hù)標(biāo)識(shí)從redis中獲取當(dāng)前用戶(hù)的基本信息围来,沒(méi)有調(diào)用會(huì)員服務(wù)獲取用戶(hù)信息航唆。并更新redis任岸。
4) 生成UsernamePasswordAuthenticationToken嗅蔬,保存到SecurityContext中。
5)把用戶(hù)基本信息ToJson為文本,保存到請(qǐng)求頭中漆诽。

4供鸠、包裝當(dāng)前請(qǐng)求類(lèi)HttpServletRequestWrapper,在當(dāng)前請(qǐng)求頭中加入登錄用戶(hù)的基本信息

public class AppendHeaderRequestWrapper extends HttpServletRequestWrapper {
    private final Map<String, String> customHeaders;

    public AppendHeaderRequestWrapper(HttpServletRequest request) {
        super(request);
        this.customHeaders = new HashMap<>();
    }

    void putHeader(String name, String value){
        this.customHeaders.put(name, value);
    }

    @Override
    public String getHeader(String name) {
        // check the custom headers first
        String headerValue = customHeaders.get(name);

        if (headerValue != null){
            return headerValue;
        }
        // else return from into the original wrapped object
        return ((HttpServletRequest) getRequest()).getHeader(name);
    }



    @Override
    public Enumeration<String> getHeaderNames() {
        // create a set of the custom header names
        Set<String> set = new HashSet<>(customHeaders.keySet());

        // now add the headers from the wrapped request object
        Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
        while (e.hasMoreElements()) {
            // add the names of the request headers into the list
            String n = e.nextElement();
            set.add(n);
        }

        // create an enumeration from the set and return
        return Collections.enumeration(set);
    }
}

5乡数、定義驗(yàn)證失敗后的處理類(lèi)

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final String TOKEN_EXPRIED = "Filter_TokenExpried";

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResultVO resultVO;
        if (httpServletRequest.getAttribute(TOKEN_EXPRIED) != null) {
            resultVO=ResultVO.fail(CoreConstants.TOKEN_EXPIRED);
        }else {
            resultVO=ResultVO.fail(CoreConstants.NEED_AUTHORITIES);
        }
        String resultJson = URLDecoder.decode(JSON.toJSONString(resultVO), "UTF-8");
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
        httpServletResponse.getWriter().write(resultJson);
    }
}

二烧栋、在Spring Cloud網(wǎng)關(guān)中魔吐,通過(guò)ZuulFilter辞色,把當(dāng)前登錄用戶(hù)的基本信息注入到請(qǐng)求頭中

@Component
public class ZuulAccessFilter extends ZuulFilter {

    private static Logger log = LoggerFactory.getLogger(ZuulAccessFilter.class);
    private static final String HEADER_USER = "key_userinfo_in_http_header";

    @Override
    public String filterType() {
        //前置過(guò)濾器  
        return "pre";
    }

    @Override
    public int filterOrder() {
        //優(yōu)先級(jí)立美,數(shù)字越大,優(yōu)先級(jí)越低  
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否執(zhí)行該過(guò)濾器蔫巩,true代表需要過(guò)濾  
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();        
        String userInfoStr = request.getHeader(HEADER_USER);
        if (!Strings.isNullOrEmpty(userInfoStr)) {            
            try {
                ctx.addZuulRequestHeader(HEADER_USER, URLEncoder.encode(userInfoStr, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        return ctx;
    }
}

三、在Spring Cloud微服務(wù)中,定義過(guò)濾器贾陷,解析網(wǎng)關(guān)傳遞的請(qǐng)求頭,解析出當(dāng)前訪問(wèn)用戶(hù)的基本信息

public class TransmitUserInfoFilter implements Filter {

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

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.initUserInfo((HttpServletRequest) request);
        chain.doFilter(request, response);
    }

    private void initUserInfo(HttpServletRequest request) {
       
        String userJson = request.getHeader("key_userinfo_in_http_header");
      
        if (StringUtils.isNotBlank(userJson)) {
            try {
                userJson = URLDecoder.decode(userJson, "UTF-8");
                UserInfo userInfo =  JSON.parseObject(userJson, UserInfo.class);
                //將UserInfo放入上下文中
                UserInfoContext.setUser(userInfo);
            } catch (UnsupportedEncodingException e) {
                log.error("init userInfo error", e);
            }
        }
    }

    @Override
    public void destroy() {
    }
}

四癣漆、定義攔截器婚肆,在服務(wù)間相互調(diào)用時(shí)糟港,把訪問(wèn)用戶(hù)的信息通過(guò)請(qǐng)求頭的方式傳遞到被調(diào)用的微服務(wù)中

public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor {

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

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //從應(yīng)用上下文中取出user信息吭敢,放入Feign的請(qǐng)求頭中
        UserInfo user = UserInfoContext.getUser();
        if (user != null) {
            try {
                String userJson = JSON.toJSONString(user);
                requestTemplate.header("KEY_USERINFO_IN_HTTP_HEADER", URLEncoder.encode(userJson, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                log.error("用戶(hù)信息設(shè)置錯(cuò)誤", e);
            }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末野宜,一起剝皮案震驚了整個(gè)濱河市其徙,隨后出現(xiàn)的幾起案子期犬,更是在濱河造成了極大的恐慌避诽,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡柒昏,警方通過(guò)查閱死者的電腦和手機(jī)有梆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)弦疮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人厕怜,你說(shuō)我怎么就攤上這事衩匣。” “怎么了粥航?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵琅捏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我递雀,道長(zhǎng)柄延,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任缀程,我火速辦了婚禮搜吧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杨凑。我一直安慰自己滤奈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布撩满。 她就那樣靜靜地躺著僵刮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鹦牛。 梳的紋絲不亂的頭發(fā)上搞糕,一...
    開(kāi)封第一講書(shū)人閱讀 50,021評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音曼追,去河邊找鬼窍仰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛礼殊,可吹牛的內(nèi)容都是我干的驹吮。 我是一名探鬼主播,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼晶伦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼碟狞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起婚陪,我...
    開(kāi)封第一講書(shū)人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤族沃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體脆淹,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡常空,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盖溺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漓糙。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖烘嘱,靈堂內(nèi)的尸體忽然破棺而出昆禽,到底是詐尸還是另有隱情,我是刑警寧澤蝇庭,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布为狸,位于F島的核電站,受9級(jí)特大地震影響遗契,放射性物質(zhì)發(fā)生泄漏辐棒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一牍蜂、第九天 我趴在偏房一處隱蔽的房頂上張望漾根。 院中可真熱鬧,春花似錦鲫竞、人聲如沸辐怕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寄疏。三九已至,卻和暖如春僵井,著一層夾襖步出監(jiān)牢的瞬間陕截,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工批什, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留农曲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓驻债,卻偏偏與公主長(zhǎng)得像乳规,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子合呐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容