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

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

1、引入Spring Security

        
            org.springframework.boot
            spring-boot-starter-security
        

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 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 getHeaderNames() {
        // create a set of the custom header names
        Set set = new HashSet<>(customHeaders.keySet());

        // now add the headers from the wrapped request object
        Enumeration 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);
            }
        }
    }
}

你可能感兴趣的:(Spring Cloud+Spring boot Sercuity+JWT进行用户认证及用户信息在微服务间的传递)