Zuul + Oauth 2 实现鉴权功能 技术简介 oauth2
OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices. This specification and its extensions are being developed within the IETF OAuth Working Group.
OAuth 2.1 is an in-progress effort to consolidate OAuth 2.0 and many common extensions under a new name.
zuul
Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. As an edge service application, Zuul is built to enable dynamic routing, monitoring, resiliency and security. It also has the ability to route requests to multiple Amazon Auto Scaling Groups as appropriate.
技术版本选择
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-zuul</artifactId > <version > 2.0.1.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > <version > 2.0.1.RELEASE</version > </dependency >
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-security</artifactId > <version > 2.0.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-oauth2</artifactId > <version > 2.0.0.RELEASE</version > </dependency >
实现
1.网关搭建
见:<<Swagger + Zuul 整合微服务接口文档>> 一文
2.oauth 配置
在 application.yml 文件中配置 auth 服务
1 2 3 4 5 6 7 8 9 10 11 12 auth: tokenValiditySeconds: 1200 clientId: domain clientSecret: domain cookieDomain: domain.com cookieMaxAge: -1 encrypt: key-store: location: classpath:rs.keystore secret: domainkeystore alias: domainkey password: domain
security 配置,放行登陆、获取验证码等接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.czq.auth.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configuration @EnableWebSecurity @Order(-1) class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin" ,"/userlogout" ,"/userjwt" ); } @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { AuthenticationManager manager = super .authenticationManagerBean(); return manager; } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Override public void configure (HttpSecurity http) throws Exception { http.csrf().disable() .httpBasic().and() .formLogin() .and() .authorizeRequests().anyRequest().permitAll(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package com.czq.auth.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.bootstrap.encrypt.KeyProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;import javax.annotation.Resource;import javax.sql.DataSource;import java.security.KeyPair;@Configuration @EnableAuthorizationServer class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired UserDetailsService userDetailsService; @Autowired AuthenticationManager authenticationManager; @Autowired TokenStore tokenStore; @Autowired private CustomUserAuthenticationConverter customUserAuthenticationConverter; @Bean("keyProp") public KeyProperties keyProperties () { return new KeyProperties (); } @Resource(name = "keyProp") private KeyProperties keyProperties; @Bean public ClientDetailsService clientDetails () { return new JdbcClientDetailsService (dataSource); } @Override public void configure (ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(this .dataSource).clients(this .clientDetails()); } @Bean @Autowired public TokenStore tokenStore (JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore (jwtAccessTokenConverter); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter (CustomUserAuthenticationConverter customUserAuthenticationConverter) { JwtAccessTokenConverter converter = new JwtAccessTokenConverter (); KeyPair keyPair = new KeyStoreKeyFactory (keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()) .getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray()); converter.setKeyPair(keyPair); DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter(); accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter); return converter; } @Override public void configure (AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.accessTokenConverter(jwtAccessTokenConverter) .authenticationManager(authenticationManager) .tokenStore(tokenStore) .userDetailsService(userDetailsService); } @Override public void configure (AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.allowFormAuthenticationForClients() .passwordEncoder(new BCryptPasswordEncoder ()) .tokenKeyAccess("permitAll()" ) .checkTokenAccess("permitAll()" ); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } }
配置自定义的 CustomUserAuthenticationConverter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.czq.auth.config;import com.czq.auth.service.UserJwt;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.Authentication;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;import org.springframework.stereotype.Component;import java.util.LinkedHashMap;import java.util.Map;@Component public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter { @Autowired UserDetailsService userDetailsService; @Override public Map<String, ?> convertUserAuthentication(Authentication authentication) { LinkedHashMap response = new LinkedHashMap (); String name = authentication.getName(); response.put("username" , name); Object principal = authentication.getPrincipal(); UserJwt userJwt = null ; if (principal instanceof UserJwt){ userJwt = (UserJwt) principal; }else { UserDetails userDetails = userDetailsService.loadUserByUsername(name); userJwt = (UserJwt) userDetails; } if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { response.put("authorities" , AuthorityUtils.authorityListToSet(authentication.getAuthorities())); } return response; } }
3.Oauth 令牌生成
在用户登录时申请 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 private AuthToken applyToken (String username, String password, String clientId, String clientSecret) throws UnsupportedEncodingException { ServiceInstance serviceInstance = loadBalancerClient.choose(ServiceList.AUTH_SERVICE); if (serviceInstance == null ){ LOGGER.error("choose an auth instance null" ); ExceptionCast.cast(AuthCode.AUTH_LOGIN_AUTHSERVER_NOTFOUND); } URI uri = serviceInstance.getUri(); String authUrl = uri+ "/auth/oauth/token" ; MultiValueMap<String,String> formData = new LinkedMultiValueMap <>(); formData.add("grant_type" ,"password" ); formData.add("username" ,username); formData.add("password" ,password); MultiValueMap<String,String> header = new LinkedMultiValueMap <>(); header.add("Authorization" ,httpbasic(clientId,clientSecret)); restTemplate.setErrorHandler(new DefaultResponseErrorHandler (){ @Override public void handleError (URI url, HttpMethod method, ClientHttpResponse response) throws IOException { if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401 ){ super .handleError(response); } } }); Map map = null ; try { HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity <>(formData, header); ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class); map = mapResponseEntity.getBody(); } catch (RestClientException e) { e.printStackTrace(); LOGGER.error("request oauth_token_password error: {}" ,e.getMessage()); e.printStackTrace(); ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL); } if (map == null || map.get("access_token" ) == null || map.get("refresh_token" ) == null || map.get("jti" ) == null ){ String error_description = (String) map.get("error_description" ); if (StringUtils.isNotEmpty(error_description)) { if (error_description.equals("坏的凭证" )){ ExceptionCast.cast(AuthCode.AUTH_CREDENTIAL_ERROR); } else if (error_description.indexOf("UserDetailsService returned null" ) > 0 ){ ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS); } } ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL); } AuthToken authToken = new AuthToken (); String jwt_token = (String) map.get("access_token" ); String refresh_token = (String) map.get("refresh_token" ); String access_token = (String) map.get("jti" ); authToken.setJwt_token(jwt_token); authToken.setAccess_token(access_token); authToken.setRefresh_token(refresh_token); return authToken;
在获取到令牌后,存储至 redis 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public AuthToken login (String username, String password, String clientId, String clientSecret) throws UnsupportedEncodingException { AuthToken authToken = applyToken(username,password,clientId,clientSecret); if (authToken == null ){ ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL); } String access_token = authToken.getAccess_token(); String content = (String) JSON.toJSONString(authToken); boolean saveTokenResult = saveToken(access_token,content,tokenValiditySeconds); if (!saveTokenResult){ ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL); } return authToken; } private boolean saveToken (String access_token, String content, long ttl) { String key = "user_token:" + access_token; stringRedisTemplate.boundValueOps(key).set(content,ttl, TimeUnit.SECONDS); Long expire = stringRedisTemplate.getExpire(key,TimeUnit.SECONDS); return expire > 0 ; }
保存到 redis 后,通过 cookie 返回至前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void saveCookie (String access_token) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); assert response != null ; CookieUtil.addCookie(response,cookieDomain,"/" , "uid" ,access_token,cookieMaxAge,false ); }
最终返回 token 至前端,前端通过携带 cookie 获取 jwt 令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public AuthToken getUserToken (String access_token) { String userToken = "user_token:" + access_token; String userTokenString = stringRedisTemplate.opsForValue().get(userToken); if (userToken != null ){ AuthToken authToken = null ; try { authToken = JSON.parseObject(userTokenString,AuthToken.class); }catch (Exception e){ LOGGER.error("getUserToken from redis and execute JSON.parseObject error {}" ,e.getMessage()); e.printStackTrace(); } return authToken; } return null ; }
4.网关配置过滤规则,放行登录等接口
在过滤规则中,对不符合要求接口予以拦截并返回 403。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package com.czq.gateway.filter;import com.alibaba.fastjson.JSON;import com.czq.common.model.response.CommonCode;import com.czq.common.model.response.ResponseResult;import com.czq.model.auth.ext.*;import com.netflix.zuul.ZuulFilter;import com.netflix.zuul.context.RequestContext;import com.netflix.zuul.exception.ZuulException;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.http.*;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.util.LinkedMultiValueMap;import org.springframework.util.MultiValueMap;import org.springframework.util.PathMatcher;import org.springframework.web.client.RestTemplate;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.ArrayList;import java.util.List;import java.util.Optional;@Slf4j @Component public class LoginFilter extends ZuulFilter { List<String> paths = new ArrayList <>(); private static final Logger LOG = LoggerFactory.getLogger(LoginFilter.class); public LoginFilter () { super (); paths.add("/**/auth/oauth/check_token" ); paths.add("/**/auth/oauth/token" ); paths.add("/**/auth/userlogin" ); paths.add("/**/auth/userjwt" ); paths.add("/**/manage/captcha" ); paths.add("/**/manage/verification" ); paths.add("/ui/**" ); paths.add("/**/swagger**/**" ); paths.add("/**/v2/api-docs" ); paths.add("/**/*.css" ); paths.add("/**/*.jpg" ); paths.add("/**/*.png" ); paths.add("/**/*.gif" ); paths.add("/**/*.js" ); paths.add("/**/*.svg" ); } @Override public String filterType () { return "pre" ; } @Override public int filterOrder () { return 1 ; } @Override public boolean shouldFilter () { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String uri = request.getRequestURI(); PathMatcher matcher = new AntPathMatcher (); Optional<String> optional = paths.stream().filter(t -> matcher.match(t, uri)).findFirst(); return !optional.isPresent(); } @Override public Object run () throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse httpServletResponse = requestContext.getResponse(); HttpServletRequest httpServletRequest = requestContext.getRequest(); String authorization = httpServletRequest.getHeader("Authorization" ); if (StringUtils.isEmpty(authorization)) { requestContext.setSendZuulResponse(false ); requestContext.setResponseStatusCode(200 ); ResponseResult unauthenticated = new ResponseResult (CommonCode.UNAUTHENTICATED); String jsonString = JSON.toJSONString(unauthenticated); requestContext.setResponseBody(jsonString); requestContext.getResponse().setContentType("application/json;charset=UTF‐8" ); return null ; } if (!StringUtils.startsWithIgnoreCase(authorization, "bearer " )) { log.error("http request header authorization is error" ); return null ; } try { TokenInfo tokenInfo = getTokenInfo(authorization); httpServletRequest.setAttribute("token" , tokenInfo); } catch (Exception e) { log.error("token check error !" , e); } return null ; } }
对携带令牌的请求通过请求鉴权服务验证 token 合法后放行至下一过滤规则,反之返回 token 检查失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private TokenInfo getTokenInfo (String authorization) { String url = "http://localhost:50201/api/v1.0.1/auth-service/auth/oauth/check_token" ; String token = StringUtils.substringAfter(authorization, "Bearer " ); HttpHeaders headers = new HttpHeaders (); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + token); headers.add(HttpHeaders.CONTENT_TYPE,"application/json" ); RestTemplateBuilder builder = new RestTemplateBuilder (); RestTemplate restTemplate = builder.basicAuthorization("RsWebApp" , "RsWebApp" ).build(); MultiValueMap<String, String> params = new LinkedMultiValueMap <>(); params.add("token" , token); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity <>(params, headers); ResponseEntity<TokenInfo> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, TokenInfo.class); return exchange.getBody(); }
5.网关鉴权过滤规则
拦截请求,校验 token 是否过期;对符合要求的请求在 header 中添加 jti (java web token identity)并予以放行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 package com.czq.gateway.filter;import com.czq.model.auth.ext.TokenInfo;import com.netflix.zuul.ZuulFilter;import com.netflix.zuul.context.RequestContext;import com.netflix.zuul.exception.ZuulException;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.util.PathMatcher;import javax.servlet.http.HttpServletRequest;import java.util.ArrayList;import java.util.List;import java.util.Optional;@Slf4j @Component public class AuthorizationFilter extends ZuulFilter { List<String> paths = new ArrayList <>(); private static final Logger LOG = LoggerFactory.getLogger(LoginFilter.class); public AuthorizationFilter () { super (); paths.add("/**/auth/oauth/check_token" ); paths.add("/**/auth/oauth/token" ); paths.add("/**/auth/userlogin" ); paths.add("/**/auth/userjwt" ); paths.add("/**/manage/captcha" ); paths.add("/**/manage/verification" ); paths.add("/ui/**" ); paths.add("/**/swagger**/**" ); paths.add("/**/v2/api-docs" ); paths.add("/**/*.css" ); paths.add("/**/*.jpg" ); paths.add("/**/*.png" ); paths.add("/**/*.gif" ); paths.add("/**/*.js" ); paths.add("/**/*.svg" ); } @Override public boolean shouldFilter () { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String uri = request.getRequestURI(); PathMatcher matcher = new AntPathMatcher (); Optional<String> optional = paths.stream().filter(t -> matcher.match(t, uri)).findFirst(); return !optional.isPresent(); } @Override public Object run () throws ZuulException { log.info("authorization start" ); RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); if (isNeedAuth(request)) { TokenInfo tokenInfo = (TokenInfo) request.getAttribute("token" ); if (tokenInfo != null && tokenInfo.isActive()) { if (!hasPermission(tokenInfo, request)) { log.info("audit log update fail 403" ); handleError(403 , requestContext); } requestContext.addZuulRequestHeader("token" , tokenInfo.getJti()); } else { if (!StringUtils.startsWith(request.getRequestURI(), "/token" )) { log.info("audit log update fail 401" ); handleError(401 , requestContext); } } } return null ; } private void handleError (int status, RequestContext requestContext) { requestContext.getResponse().setContentType("application/json" ); requestContext.setResponseStatusCode(status); requestContext.setResponseBody("{\"message\":\"auth fail\"}" ); requestContext.setSendZuulResponse(false ); } private boolean hasPermission (TokenInfo tokenInfo, HttpServletRequest request) { return true ; } private boolean isNeedAuth (HttpServletRequest request) { return true ; } @Override public String filterType () { return "pre" ; } @Override public int filterOrder () { return 3 ; } }
TokenInfo.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.czq.model.auth.ext;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;import java.util.List;@Data @ToString @NoArgsConstructor public class TokenInfo { String userpic; String user_name; List<String> scope; String name; String utype; boolean active; String id; long exp; String jti; String client_id; }
AuthToken.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.czq.model.auth.ext;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;@Data @ToString @NoArgsConstructor public class AuthToken { String access_token; String refresh_token; String jwt_token; }