网关解析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;
}
}
资源服务的代码和上一种完全一样