Spring Cloud+Spring boot Sercuity+JWT进行用户认证及用户信息在微服务间的传递

一、spring boot Security+JWT 在Spring Cloud网关层实现用户认证

1、引入Spring Security

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

2、创建WebSecurityConfig继承WebSecurityConfigurerAdapter
重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web应用安全配置的适配器。

@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()             
                // 对于获取token的rest api要允许匿名访问
                .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文档无授权访问

                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

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

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

Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。其中UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录认证。我们通过该过滤器,实现JWT的用户认证。

3、创建JWTAuthenticationTokenFilter 过滤器,实现对请求token的用户认证

@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);
        }
    }    
}

该过滤器的处理步骤:
1)判断httpmothod为“OPTIONS”,直接放通,允许跨域访问。
2)判断请求header中是否存在指定名的token,调用JWT工具类获取当前用户标识,判断token是否过期。
3)根据用户标识从redis中获取当前用户的基本信息,没有调用会员服务获取用户信息。并更新redis。
4) 生成UsernamePasswordAuthenticationToken,保存到SecurityContext中。
5)把用户基本信息ToJson为文本,保存到请求头中。

4、包装当前请求类HttpServletRequestWrapper,在当前请求头中加入登录用户的基本信息

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、定义验证失败后的处理类

@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网关中,通过ZuulFilter,把当前登录用户的基本信息注入到请求头中

@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() {
        //前置过滤器  
        return "pre";
    }

    @Override
    public int filterOrder() {
        //优先级,数字越大,优先级越低  
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否执行该过滤器,true代表需要过滤  
        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微服务中,定义过滤器,解析网关传递的请求头,解析出当前访问用户的基本信息

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() {
    }
}

四、定义拦截器,在服务间相互调用时,把访问用户的信息通过请求头的方式传递到被调用的微服务中

public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor {

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

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //从应用上下文中取出user信息,放入Feign的请求头中
        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("用户信息设置错误", e);
            }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容