网关解析oauth2颁发的令牌,并将明文转发给资源服务

网关解析oauth2颁发的令牌,并将明文转发给资源服务

1 目标

认证服务颁发oauth令牌,请求资源的时候携带该令牌,令牌统一在网关解析,网关解析完成后,将明文经过base64编码之后放入请求头中转发至资源服务

2 oauth2解析令牌过程

oauth有一个jwt令牌转换器,这个转换器可以生成和解析令牌,现在我们就去看看它是如何解析令牌的

其实,它和security差不多,也是在过滤器链中被一个过滤器解析的,这个过滤器就是OAuth2AuthenticationProcessingFilter

我们看一下这个过滤器是怎么解析的

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {

    private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);

    private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

    private AuthenticationManager authenticationManager;

    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();

    private TokenExtractor tokenExtractor = new BearerTokenExtractor();

    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

    private boolean stateless = true;


    /**
     * 过滤器核心方法
     */
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
    ServletException {

        final boolean debug = logger.isDebugEnabled();
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        try {

            //看这个方法,这个方法的作用是判断请求中是否携带Authorization的参数,有就将这个值取出封装到Authentication中
            Authentication authentication = tokenExtractor.extract(request);

            if (authentication == null) {
                //不是oauth2的请求,直接放行
                if (stateless && isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }
                    SecurityContextHolder.clearContext();
                }
                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            }
            else {
                //oauth2请求
                //给令牌重新取个名字放到请求参数中
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    //authentication的实际类型是PreAuthenticatedAuthenticationToken,是它的子类
                    //强转为父类
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    /**
                     *根据请求构建一个details
                     *authenticationDetailsSource.buildDetails(request),这个很重要,我们在资源服务构建认证对象的时
                     *候,除了要将用户名和权限封装到认证对象中,还需要一个details,而这行代码就是教我们怎么根据自己的请求构
                     *建一个details
                     */
                    needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
                }
                //这个就是解析令牌封装认证对象了,最核心的方法
                Authentication authResult = authenticationManager.authenticate(authentication);

                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                eventPublisher.publishAuthenticationSuccess(authResult);
                //封装好的认证对象保存到securityContext容器中
                SecurityContextHolder.getContext().setAuthentication(authResult);

            }
        }
        catch (OAuth2Exception failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                logger.debug("Authentication request failed: " + failed);
            }
            eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                                                        new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

            authenticationEntryPoint.commence(request, response,
                                              new InsufficientAuthenticationException(failed.getMessage(), failed));

            return;
        }

        chain.doFilter(request, response);
    }

}

解析就已经完成了,oauth的认证对象也放到了securityContext容器中,我们如果有需要的的话

就可以使用

//获取容器中的认证对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//判断认证对象的类型
if (authentication instanceof OAuth2Authentication){
    //实际类型为oauth的认证对象
    
}

2.1 判断是否oauth请求

//tokenExtractor的类型是BearerTokenExtractor
Authentication authentication = tokenExtractor.extract(request);

分析一下BearerTokenExtractor的源码

public class BearerTokenExtractor implements TokenExtractor {

    private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);

    /**
     * 调用的是这个方法
     */
    @Override
    public Authentication extract(HttpServletRequest request) {
        //获取请求中Authorization的值
        String tokenValue = extractToken(request);
        if (tokenValue != null) {
            /**
     		 * 这里已经指明了authentication的实际类型是PreAuthenticatedAuthenticationToken
     		 * 并且他将Authorization的值封装到了这个对象中
     		 */
            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
            return authentication;
        }
        return null;
    }

    protected String extractToken(HttpServletRequest request) {
        // first check the header...
        String token = extractHeaderToken(request);

        // bearer type allows a request parameter as well
        if (token == null) {
            logger.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
            if (token == null) {
                logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
            }
        }

        return token;
    }

    /**
	 * Extract the OAuth bearer token from a header.
	 * 
	 * @param request The request.
	 * @return The token, or null if no OAuth authorization header was supplied.
	 */
    protected String extractHeaderToken(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaders("Authorization");
        while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
            String value = headers.nextElement();
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // Add this here for the auth details later. Would be better to change the signature of this method.
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
                                     value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }

        return null;
    }

}

2.2 解析令牌构建oauth认证对象

//解析令牌构建认证对象
Authentication authResult = authenticationManager.authenticate(authentication);

debug一下很容易知道authenticationManager的实际类型是OAuth2AuthenticationManager

public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {

	private ResourceServerTokenServices tokenServices;

	private ClientDetailsService clientDetailsService;

	private String resourceId;

	
	/**
	 * Expects the incoming authentication request to have a principal value that is an access token value (e.g. from an
	 * authorization header). Loads an authentication from the {@link ResourceServerTokenServices} and checks that the
	 * resource id is contained in the {@link AuthorizationRequest} (if one is specified). Also copies authentication
	 * details over from the input to the output (e.g. typically so that the access token value and request details can
	 * be reported later).
	 * 
	 * @param authentication an authentication request containing an access token value as the principal
	 * @return an {@link OAuth2Authentication}
	 * 
	 * @see org.springframework.security.authentication.AuthenticationManager#authenticate(org.springframework.security.core.Authentication)
	 */
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		if (authentication == null) {
			throw new InvalidTokenException("Invalid token (token not found)");
		}
        //得到令牌
		String token = (String) authentication.getPrincipal();
        //又封装了。。。我们看这个方法
		OAuth2Authentication auth = tokenServices.loadAuthentication(token);
		if (auth == null) {
			throw new InvalidTokenException("Invalid token: " + token);
		}

        //资源id相关
		Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
		if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
            //从这里我们可以看到oauth2的令牌必须设置资源id
			throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
		}

		checkClientDetails(auth);

        /**
         * 解析令牌构建oauth认证对象之前根据请求构建了一个details
         *  needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
         */
		if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
			OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
			// Guard against a cached copy of the same details
			if (!details.equals(auth.getDetails())) {
				// Preserve the authentication details from the one loaded by token services
				details.setDecodedDetails(auth.getDetails());
			}
		}
		auth.setDetails(authentication.getDetails());
        //认证成功
		auth.setAuthenticated(true);
		return auth;

	}

}

2.3 解析令牌

debug发现tokenServices中的实际对象是DefaultTokenServices

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {

    //刷新令牌的有效时间
    private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

    //默认的令牌有效时间
    private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

    private boolean supportRefreshToken = false;

    private boolean reuseRefreshToken = true;

    private TokenStore tokenStore;

    private ClientDetailsService clientDetailsService;

    private TokenEnhancer accessTokenEnhancer;

    private AuthenticationManager authenticationManager;


    /**
     * 解析令牌了
     */
    public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
    InvalidTokenException {
        //tokenStore的真实类型是JwtTokenStore
        //第一步,读取令牌,封装令牌的相关信息
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
        if (accessToken == null) {
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        else if (accessToken.isExpired()) {
            tokenStore.removeAccessToken(accessToken);
            throw new InvalidTokenException("Access token expired: " + accessTokenValue);
        }

        //tokenStore的真实类型是JwtTokenStore
         //第二步,根据令牌信息得到oauth的认证对象
        OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
        if (result == null) {
            // in case of race condition
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        //验证客户端id是否有效
        if (clientDetailsService != null) {
            String clientId = result.getOAuth2Request().getClientId();
            try {
                clientDetailsService.loadClientByClientId(clientId);
            }
            catch (ClientRegistrationException e) {
                throw new InvalidTokenException("Client not valid: " + clientId, e);
            }
        }
        return result;
    }
}
2.3.1 第一步,读取令牌,封装令牌的相关信息

看readAccessToken方法源码

public class JwtTokenStore implements TokenStore {

	private JwtAccessTokenConverter jwtTokenEnhancer;

	private ApprovalStore approvalStore;


	@Override
	public OAuth2AccessToken readAccessToken(String tokenValue) {
        //调用下面方法,将令牌解析封装成OAuth2AccessToken对象
		OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
		if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
			throw new InvalidTokenException("Encoded token is a refresh token");
		}
		return accessToken;
	}

	private OAuth2AccessToken convertAccessToken(String tokenValue) {
        //这个方法就是验签并解析令牌,封装成OAuth2AccessToken
		return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
	}
}

真正解析令牌的是这个方法,jwtTokenEnhancer就是JwtAccessTokenConverter,这个不就是我们在oauth配置类中配置的令牌转换器吗

jwtTokenEnhancer.decode(tokenValue)

我们看一下decode方法源码

protected Map<String, Object> decode(String token) {
    try {
        //验签并解析
        Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
        //获取载荷
        String claimsStr = jwt.getClaims();
        Map<String, Object> claims = objectMapper.parseMap(claimsStr);
        //格式化令牌过期时间
        if (claims.containsKey(EXP) && claims.get(EXP) instanceof Integer) {
            Integer intValue = (Integer) claims.get(EXP);
            claims.put(EXP, new Long(intValue));
        }
        //这个啥也没干
        this.getJwtClaimsSetVerifier().verify(claims);
        return claims;
    }
    catch (Exception e) {
        throw new InvalidTokenException("Cannot convert access token to JSON", e);
    }
}

解析完成后,它会把令牌的内容封装成一个map集合,再由extractAccessToken方法将其封装成OAuth2AccessToken对象

@Override
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
    //调用的是DefaultAccessTokenConverter中的extractAccessToken方法
    return tokenConverter.extractAccessToken(value, map);
}

看源码

public class DefaultAccessTokenConverter implements AccessTokenConverter {

	private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
	
	private boolean includeGrantType;

	private String scopeAttribute = SCOPE;

	private String clientIdAttribute = CLIENT_ID;

	
	public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
        //原始的令牌也要封装进去
		DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(value);
		Map<String, Object> info = new HashMap<String, Object>(map);
		info.remove(EXP);
		info.remove(AUD);
		info.remove(clientIdAttribute);
		info.remove(scopeAttribute);
        //过期时间
		if (map.containsKey(EXP)) {
			token.setExpiration(new Date((Long) map.get(EXP) * 1000L));
		}
		if (map.containsKey(JTI)) {
			info.put(JTI, map.get(JTI));
		}
        //客户端权限
		token.setScope(extractScope(map));
		token.setAdditionalInformation(info);
		return token;
	}
}
2.3.2 第二步,根据令牌信息得到oauth的认证对象

现在我们回到loadAuthentication这个方法,看第二步操作

//tokenStore的真实类型是JwtTokenStore
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);

进入readAuthentication方法源码

@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
    //token.getValue()未解析的令牌
    return readAuthentication(token.getValue());
}

@Override
public OAuth2Authentication readAuthentication(String token) {
    /**
     * jwtTokenEnhancer.decode(token) 把令牌又解析了一次
     * jwtTokenEnhancer的实际类型是JwtAccessTokenConverter
     */
    return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));
}

去JwtAccessTokenConverter类的extractAuthentication方法

@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
    //tokenConverter的实际类型是DefaultAccessTokenConverter
    return tokenConverter.extractAuthentication(map);
}

去DefaultAccessTokenConverter类的extractAuthentication方法看看它对令牌解析后的数据做了啥

public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
    Map<String, String> parameters = new HashMap<String, String>();
    //取出客户端权限
    Set<String> scope = extractScope(map);
    /**
     * private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
     * userTokenConverter的实际类型是DefaultUserAuthenticationConverter
     * 我们看一看这个方法是如何封装一个security认证对象的,如果我们以后需要自己解析令牌的话,就可以按照这个流程封装了
     */
    Authentication user = userTokenConverter.extractAuthentication(map);
    //客户端id
    String clientId = (String) map.get(clientIdAttribute);
    parameters.put(clientIdAttribute, clientId);
    if (includeGrantType && map.containsKey(GRANT_TYPE)) {
        parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
    }
    //资源id,都是集合
    Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? getAudience(map)
                                                        : Collections.<String>emptySet());

    
    Collection<? extends GrantedAuthority> authorities = null;
    if (user==null && map.containsKey(AUTHORITIES)) {
        //客户端认证模式
        @SuppressWarnings("unchecked")
        String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
        //这里有个工具类,轻松生成一个权限对象,以后我们会用到
        authorities = AuthorityUtils.createAuthorityList(roles);
    }
    //这两行就是封装oauth的认证对象了
    OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
                                              null);
    return new OAuth2Authentication(request, user);
}
2.3.2.1 封装一个security认证对象
Authentication user = userTokenConverter.extractAuthentication(map);

去DefaultUserAuthenticationConverter类的extractAuthentication方法

public Authentication extractAuthentication(Map<String, ?> map) {
    if (map.containsKey(USERNAME)) {
        Object principal = map.get(USERNAME);
        Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
        if (userDetailsService != null) {
            //调方法从数据库查询用户信息
            UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
            authorities = user.getAuthorities();
            //从这时候开始,principal就不是用户名了,而是一个用户对象,包含了其他的信息
            principal = user;
        }
        //返回security认证对象
        return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
    }
    return null;
}
2.3.2.2 封装一个oauth认证对象
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,null);
return new OAuth2Authentication(request, user);

3 实现

所有携带了令牌的请求都在这里先将令牌解析,把令牌内容经过base64加密后转发给微服务

我们这里网关使用zuul

3.1 application.yml

server:
  port: 10001

spring:
  main:
    allow-bean-definition-overriding: true

  application:
    name: zuul

logging:
  level:
    cn.lx.security: debug

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true

zuul:
  #是否去除前缀
  strip-prefix: true
  routes:
    #模块的别名
    resource2:
      #配置请求URL的请求规则(只能写一个)
      path: /resource2/**
      #指定Eureka注册中心中的服务id(需要打开从eureka获取数据的配置fetch-registery)
      serviceId: resource2
      sentiviteHeaders:
      #处理cookie及重定向问题
      customSensitiveHeaders: true

    #模块的别名
    server:
      #配置请求URL的请求规则(只能写一个)
      path: /server/**
      #指定Eureka注册中心中的服务id(需要打开从eureka获取数据的配置fetch-registery)
      serviceId: server
      sentiviteHeaders:
      #处理cookie及重定向问题
      customSensitiveHeaders: true

3.2 启动类

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }

}

3.3 需要的工具类

//模仿oauth解析令牌的源码写的
public class JwtTokenUtil {


    /**
     * 解析令牌
     * @param token
     * @return
     */
    public static Map<String, Object> decodeJwtToken(String token) {
        try {
            //获取公钥
            String rsaPublicKey = KeyUtil.readKey("publicKey.txt");
            //验签
            Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(rsaPublicKey));
            String claimsStr = jwt.getClaims();
            Map<String, Object> claims = JSON.parseObject(claimsStr,Map.class);
            //令牌是否过期
            if (claims.containsKey("exp") && claims.get("exp") instanceof Integer) {
                Integer intValue = (Integer) claims.get("exp");
                claims.put("exp", new Long(intValue));
                Date expiration=new Date(new Long(intValue));
                if(!expiration.before(new Date())){
                    throw new RuntimeException("令牌已经过期了");
                }
            }
            //this.getJwtClaimsSetVerifier().verify(claims);
            return claims;
        }
        catch (Exception e) {
            throw new RuntimeException("Cannot convert access token to JSON");
        }
    }
}

public class KeyUtil {

    /**
     * 读取秘钥
     * @param keyName
     * @return
     */
    public static String readKey(String keyName){
        ClassPathResource resource=new ClassPathResource(keyName);
        String key =null;
        try {
            InputStream is = resource.getInputStream();
            key = StreamUtils.copyToString(is, Charset.defaultCharset());
            key = StringUtils.replace(key, "\r", "");
            key = StringUtils.replace(key, "\n", "");
        }catch (Exception e){
            throw new RuntimeException("读取秘钥错误");
        }
        if (key==null){
            throw new RuntimeException("秘钥为空");
        }
       return key;
    }
}
public class ResponseUtil {

    /**
     * 将结果以json格式返回
     * @param result    返回结果
     * @param response
     * @throws IOException
     */
    public static void responseJson(Result result, HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(200);
        PrintWriter writer = response.getWriter();
        writer.write(JSON.toJSONString(result));
        writer.flush();
        writer.close();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {

    private String code;
    private String msg;
    private Object data;

    public Result(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

3.4 第一种实现方式:自己解析令牌然后转发

zuul网关使用很简单,就是继承ZuulFilter,然后重写run()

@Component
public class JwtDecodeFilter extends ZuulFilter {
    /**
     * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
     * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
     * We also support a "static" type for static responses see  StaticResponseFilter.
     * Any filterType made be created or added and run by calling FilterProcessor.runFilters(type)
     *
     * @return A String representing that type
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * filterOrder() must also be defined for a filter. Filters may have the same  filterOrder if precedence is not
     * important for a filter. filterOrders do not need to be sequential.
     *
     * @return the int order of a filter
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * a "true" return from this method means that the run() method should be invoked
     *
     * @return true if the run() method should be invoked. false will not invoke the run() method
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter
     *
     * @return Some arbitrary artifact may be returned. Current implementation ignores it.
     * @throws ZuulException if an error occurs during execution.
     */
    @Override
    public Object run() throws ZuulException {

        //获取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        HttpServletResponse response = currentContext.getResponse();

        String requestURI = request.getRequestURI();
        if (requestURI.startsWith("/server")){
            //认证服务不做任何操作,直接转发
            return null;
        }
        //获取令牌
        String authorization = request.getHeader("Authorization");
        if (authorization==null|| StringUtils.isEmpty(authorization)||!authorization.startsWith("Bearer ")){
            //不需要路由,默认是true
            currentContext.setSendZuulResponse(false);
            try {
                ResponseUtil.responseJson(new Result("403","用户未登录,请先登录!"),response);
            } catch (IOException e) {
               throw new RuntimeException("请求异常");
            }
            //直接返回
            return null;
        }

        //解析令牌
        Map<String, Object> map = JwtTokenUtil.decodeJwtToken(authorization.substring(7));
        //必须要有用户名
        if (map==null||map.get("user_name")==null){
            //不需要路由,默认是true
            currentContext.setSendZuulResponse(false);
            try {
                ResponseUtil.responseJson(new Result("403","用户未登录,请先登录!"),response);
            } catch (IOException e) {
                throw new RuntimeException("请求异常");
            }
            //直接返回
            return null;
        }

        //将用户名和权限添加到请求头中
        Map<String,Object> jsonToken=new HashMap<>();
        jsonToken.put("user_name",map.get("user_name"));
        jsonToken.put("authorities",map.get("authorities"));
        //base64加密后转发
        currentContext.addZuulRequestHeader("jsonToken", Base64.getEncoder()
                .encodeToString(JSON.toJSONString(jsonToken).getBytes()));
        return null;
    }
}

这种方法是将网关直接解析jwt令牌,所以我们只需要导入一个包

<!--不需要security和oauth2的包-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

因为我们在网关解析的时候,只将用户相关的信息(没发客户端信息)发送到了资源服务,那么同样资源服务也就不需要oauth2的包了,我们使用security认证,在过滤器中构建一个security的认证对象放到securityContext容器中

资源服务中自定义一个过滤器,继承OncePerRequestFilter过滤器

@Component
public class AuthorizationFilter extends OncePerRequestFilter {
    /**
     * Same contract as for {@code doFilter}, but guaranteed to be
     * just invoked once per request within a single request thread.
     * See {@link #shouldNotFilterAsyncDispatch()} for details.
     * <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
     * default ServletRequest and ServletResponse ones.
     *
     * @param request
     * @param response
     * @param filterChain
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中明文信息
        String jsonToken = request.getHeader("jsonToken");
        if (jsonToken==null|| StringUtils.isEmpty(jsonToken)){
            //没有信息
            throw new RuntimeException("用户未登录,请先登录");
        }
        String userInfo = new String(Base64.getDecoder().decode(jsonToken), "UTF-8");
        //得到用户名和用户权限
        JSONObject jsonObject = JSON.parseObject(userInfo);
        String user_name = jsonObject.getString("user_name");
        JSONArray authorities = jsonObject.getJSONArray("authorities");
        //使用工具类转化权限
        String[] array = authorities.toArray(new String[0]);
        List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(array);
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                user_name, null,authorityList);
        //已通过认证的对象放入容器中
        SecurityContextHolder.getContext().setAuthentication(authRequest);
        filterChain.doFilter(request,response);
    }
}

我们需要在security的配置文件中将这个过滤器加入到过滤器链中

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

    @Autowired
    private AuthorizationFilter authorizationFilter;
    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //将自定义过滤器加到过滤器链中
                .and().addFilterAt(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

3.5 第二种实现方式:使用oauth自带的令牌解析

oauth可以自动解析令牌,它也是将认证成功的对象放入securityContext容器中。所以我们可以将网关作为oauth的客户端,等认证成功后,在网关过滤器中取出认证对象中的信息,然后再转发

我们的网关首先需要导入这两个包

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

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

创建security的配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                //禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ;
    }
}

在oauth的配置类中放行认证服务请求

@Configuration
//记住这个注解,继承的类从注解源码注释找
@EnableResourceServer
public class OauthResourceConfig  extends ResourceServerConfigurerAdapter {


    /**
     * 告诉服务器令牌的类型是jwt
     * @return
     */
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 告诉服务器令牌如何验证
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        String publicKey = KeyUtil.readKey("publicKey.txt");
        //设置rsa公钥
        jwtAccessTokenConverter.setVerifierKey(publicKey);
        return jwtAccessTokenConverter;
    }

    /**
     * 设置资源标识,并设置令牌验证策略
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                //很重要,唯一标识这个资源服务器
                .resourceId("product_api")
                .tokenStore(tokenStore());
    }

    /**
     * 和security中的同名方法差不多
     * 这里面可以配置客户端的访问权限
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                //前后端分离,禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //认证服务的请求全部放行
                .and().authorizeRequests().antMatchers("/server/**").permitAll()
                .and().authorizeRequests()
                //客户端权限为read才能使用get方法,读取资源
                .antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')")
                //客户端权限为write能使用post方法,修改资源
                .antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')");
    }
}

接下来就是网关过滤器了,在过滤器中我们将oauth的认证对象取出,获取到用户名和权限,经过base64编码转发给资源服务

@Component
public class SendJsonTokenFilter extends ZuulFilter {
    /**
     * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
     * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
     * We also support a "static" type for static responses see  StaticResponseFilter.
     * Any filterType made be created or added and run by calling FilterProcessor.runFilters(type)
     *
     * @return A String representing that type
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * filterOrder() must also be defined for a filter. Filters may have the same  filterOrder if precedence is not
     * important for a filter. filterOrders do not need to be sequential.
     *
     * @return the int order of a filter
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * a "true" return from this method means that the run() method should be invoked
     *
     * @return true if the run() method should be invoked. false will not invoke the run() method
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter
     *
     * @return Some arbitrary artifact may be returned. Current implementation ignores it.
     * @throws ZuulException if an error occurs during execution.
     */
    @Override
    public Object run() throws ZuulException {

        RequestContext currentContext = RequestContext.getCurrentContext();

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (!(authentication instanceof OAuth2Authentication)){
            //说明请求不是oauth请求,没有携带令牌
            //直接放行
            return null;
        }
        //强转为真实类型
        OAuth2Authentication oAuth2Authentication= (OAuth2Authentication) authentication;
        //取出用户名和用户权限
        UsernamePasswordAuthenticationToken userAuthentication = (UsernamePasswordAuthenticationToken)
                oAuth2Authentication.getUserAuthentication();
        Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
        String principal = (String) userAuthentication.getPrincipal();
        //获取权限
        Set<String> authoritySet = AuthorityUtils.authorityListToSet(authorities);
        Map<String,Object> jsonToken=new HashMap<>();
        jsonToken.put("user_name",principal);
        jsonToken.put("authorities",authoritySet);
        currentContext.addZuulRequestHeader("jsonToken", Base64.getEncoder()
                .encodeToString(JSON.toJSONString(jsonToken).getBytes()));
        return null;
    }
}

资源服务的代码和上一种完全一样

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot Security OAuth2是基于Spring Security和OAuth2的框架,用于实现授权服务器和资源服务器。JWT令牌是一个基于JSON的开放标准,用于在各方之间安全地传输信息。 在Spring Boot Security OAuth2中实现支持JWT令牌的授权服务器,可以按照以下步骤进行: 1. 添加依赖:在项目的pom.xml文件中添加Spring Security OAuth2和JWT的相关依赖。 2. 配置授权服务器:在Spring Boot应用程序的配置文件中,配置授权服务器的基本设置,包括端点URL、客户端信息、用户认证信息等。 3. 配置JWT令牌:配置JWT令牌的签名密钥和过期时间等信息。可以使用开源库如jjwt来生成和验证JWT令牌。 4. 创建自定义的认证提供程序:实现自定义的认证提供程序来支持JWT令牌的认证机制。在认证提供程序中,可以使用JWT令牌解析并验证请求中的令牌信息。 5. 创建自定义的用户详细信息服务:实现自定义的用户详细信息服务,用于从数据库或其他存储中获取用户的详细信息。在用户详细信息服务中,可以根据JWT令牌中的信息获取用户信息。 6. 配置授权服务器的访问规则:配置授权服务器的访问规则,包括允许或禁止特定角色或权限的访问。 7. 测试访问授权服务器:使用客户端应用程序发送请求到授权服务器的端点,获取JWT令牌并验证其有效性。 通过以上步骤,可以实现一个支持JWT令牌的授权服务器。该服务器可以提供为客户端应用程序颁发和验证JWT令牌的功能,以实现安全并可靠的用户认证和授权控制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值