做网站一般需要什么/怎样宣传自己的产品
网关解析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);/*** 调用的是这个方法*/@Overridepublic 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 wellif (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的令牌必须设置资源idthrow 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 detailsif (!details.equals(auth.getDetails())) {// Preserve the authentication details from the one loaded by token servicesdetails.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 conditionthrow 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;@Overridepublic 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) {//这个方法就是验签并解析令牌,封装成OAuth2AccessTokenreturn 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的实际类型是DefaultAccessTokenConverterreturn 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);//客户端idString 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: 10001spring:main:allow-bean-definition-overriding: trueapplication:name: zuullogging:level:cn.lx.security: debugeureka:client:service-url:defaultZone: http://127.0.0.1:7001/eurekainstance:prefer-ip-address: truezuul:#是否去除前缀strip-prefix: trueroutes:#模块的别名resource2:#配置请求URL的请求规则(只能写一个)path: /resource2/**#指定Eureka注册中心中的服务id(需要打开从eureka获取数据的配置fetch-registery)serviceId: resource2sentiviteHeaders:#处理cookie及重定向问题customSensitiveHeaders: true#模块的别名server:#配置请求URL的请求规则(只能写一个)path: /server/**#指定Eureka注册中心中的服务id(需要打开从eureka获取数据的配置fetch-registery)serviceId: serversentiviteHeaders:#处理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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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.*/@Overridepublic 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 ")){//不需要路由,默认是truecurrentContext.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){//不需要路由,默认是truecurrentContext.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*/@Overrideprotected 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 {@Autowiredprivate 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*/@Overrideprotected 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*/@Overrideprotected 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*/@Beanpublic TokenStore tokenStore(){return new JwtTokenStore(jwtAccessTokenConverter());}/*** 告诉服务器令牌如何验证* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();String publicKey = KeyUtil.readKey("publicKey.txt");//设置rsa公钥jwtAccessTokenConverter.setVerifierKey(publicKey);return jwtAccessTokenConverter;}/*** 设置资源标识,并设置令牌验证策略* @param resources* @throws Exception*/@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources//很重要,唯一标识这个资源服务器.resourceId("product_api").tokenStore(tokenStore());}/*** 和security中的同名方法差不多* 这里面可以配置客户端的访问权限* @param http* @throws Exception*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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.*/@Overridepublic 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;}
}
资源服务的代码和上一种完全一样