在原本的单体应用中,通常使用 Apache Shiro、Spring Security 等权限框架,但是在 Spring Cloud 中,面对成千上万的微服务,而且每个服务之间无状态,使用 Shiro、Security 难免力不从心。在解决方案的选择上,传统的单点登录SSO、分布式 session 等,要么致使权限服务器集中化,导致流量臃肿,要么需要实现一套复杂的存储同步机制,都不是最好的解决方案。
可以使用 Spring Cloud Zuul 自定义实现权限认证方式
源码:https://gitee.com/laiyy0728/spring-cloud/tree/master/spring-cloud-zuul/spring-cloud-zuul-security
自定义权限认证
Filter
Zuul 对于请求的转发是通过 Filter 链控制的,可以在 RequestContext 的基础上做任何事。所以只需要在 spring-cloud-zuul-filter
的基础上,设置一个执行顺序比较靠前的 Filter,就可以专门用于对请求特定内容做权限认证。
优点:实现灵活度高,可整合已有的权限系统,对原始系统违法化友好
缺点:需要开发一套新的逻辑,维护成本增加,调用链紊乱
OAuth2.0 + JWT
OAuth2.0 是对于“授权-认证”比较成熟的面向资源的授权协议。整个授权流程中,用户是资源拥有者,服务端需要资源拥有者的授权,这个过程相当于键入密码或者其他第三方登录。触发了这个操作后,客户端就可以向授权服务器申请 Token,拿到后,再携带 Token 到资源所在服务器拉取响应资源。
JWT(JSON Web Token)是一种使用 JSON 格式来规范 Token 或 Session 的协议。由于传统认证方式会生成一个凭证,这个凭证可以是 Token 或 Session,保存于服务端或其他持久化工具中,这样一来,凭证的存取或十分麻烦。JWT 实现了“客户端 Session”。
JWT 的组成部分:
- Header 头部:指定 JWT 使用的签名算法
- Payload 载荷:包含一些自定义与非自定义的认证信息
- Signature:将头部、载荷使用“.”连接后,使用头部的签名算法生成签名信息,并拼装到末尾
OAuth2.0 + JWT 的意义在于,使用 OAuth2.0 协议思想拉取认证生成 TToken,使用 JWT 瞬时保存这个 Token,在客户端与资源端进行对称或非对称加密,是的这个规约具有定时、定量的授权认证功能,从而免去 Token 存储带来的安全或者系统扩展问题。
实现
Zuul Server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies>
|
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
| spring: application: name: spring-cloud-zuul-security-server server: port: 5555 eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true instance-id: ${spring.application.name}:${server.port} zuul: routes: spring-cloud-zuul-security-provider-service: path: /provider/** serviceId: spring-cloud-zuul-security-provider-service security: oauth2: client: access-token-uri: http://localhost:7777/uaa/oauth/token user-authorization-uri: http://localhost:7777/uaa/oauth/authorize client-id: zuul_server client-secret: secret resource: jwt: key-value: spring-cloud
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy @EnableOAuth2Sso public class SpringCloudZuulSecurityServerApplication {
public static void main(String[] args) { SpringApplication.run(SpringCloudZuulSecurityServerApplication.class, args); }
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/provider/**") .permitAll().anyRequest().authenticated().and().csrf().disable(); } }
|
auth server
1 2 3 4 5 6 7 8 9 10
| <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| spring: application: name: spring-cloud-zuul-security-auth-server server: port: 7777 servlet: context-path: /uaa eureka: instance: instance-id: ${spring.application.name}:${server.port} prefer-ip-address: true client: service-url: defaultZone: http://localhost:8761/eureka/
|
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
| @SpringBootApplication @EnableDiscoveryClient public class SpringCloudZuulSecurityAuthServerApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) { SpringApplication.run(SpringCloudZuulSecurityAuthServerApplication.class, args); }
@Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean public static PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("guest").password("guest").authorities("WRIGHT_READ") .and() .withUser("admin").password("admin").authorities("WRIGHT_READ", "WRIGHT_WRITE"); } }
@Configuration @EnableAuthorizationServer public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
@Autowired public OAuthConfiguration(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; }
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("zuul_server") .secret("secret") .scopes("WRIGHT", "READ") .autoApprove(true) .authorities("WRIGHT_READ", "WRIGHT_WRITE") .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code"); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(jwtTokenStore()) .tokenEnhancer(jwtAccessTokenConverter()) .authenticationManager(authenticationManager); }
@Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); }
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey("spring-cloud"); return jwtAccessTokenConverter; } }
|
provider
1 2 3 4 5 6 7 8 9 10
| <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12
| server: port: 7070 spring: application: name: spring-cloud-zuul-security-provider-service eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true instance-id: ${spring.application.name}:${server.port}
|
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
| @SpringBootApplication @EnableDiscoveryClient @RestController @EnableResourceServer public class SpringCloudZuulSecurityProviderServiceApplication extends ResourceServerConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringCloudZuulSecurityProviderServiceApplication.class);
public static void main(String[] args) { SpringApplication.run(SpringCloudZuulSecurityProviderServiceApplication.class, args); }
@GetMapping(value = "/test") public String test(HttpServletRequest request) { LOGGER.info(">>>>>>>>>>>>>>>>>>>>>>>>> header start! <<<<<<<<<<<<<<<<<<<<<<"); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String header = headerNames.nextElement(); String value = request.getHeader(header); LOGGER.info(">>>>>>>>>>>>>>>> {} : {} <<<<<<<<<<<<<<<<<<<", header, value); } LOGGER.info(">>>>>>>>>>>>>>>>>>>>>>>>> header end! <<<<<<<<<<<<<<<<<<<<<<"); return " test!"; }
@Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable().authorizeRequests().antMatchers("/**").authenticated() .antMatchers(HttpMethod.GET, "/test") .hasAuthority("WRIGHT_READ"); }
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("WRIGHT") .tokenStore(tokenStore()); }
@Bean public TokenStore tokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); }
@Bean protected JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("spring-cloud"); return converter; } }
|
验证
访问 http://localhost:5555/provider/test 页面返回值如下
访问 http://localhost:5555/login 将会自动跳转到 http://localhost:7777/uaa/login 使用 admin/admin
登录
访问成功后会返回一个 404 页面,这是因为没有配置成功后跳转页面导致的,暂时不管
再次访问 http://localhost:5555/provider/test 页面返回值如下
同时查看 provider-service,控制台输出如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| >>>>>>>>>>>>>>>>>>>>>>>> header start! <<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> upgrade-insecure-requests : 1 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> user-agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> dnt : 1 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> accept-language : zh-CN,zh;q=0.9 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> authorization : bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTA2MDcxODQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiV1JJR0hUX1dSSVRFIiwiV1JJR0hUX1JFQUQiXSwianRpIjoiZTJjYmNjNDk tMzE5ZC00NDdhLTlmMWYtZmY0YzI5ZDFmZWM4IiwiY2xpZW50X2lkIjoic3ByaW5nLWNsb3VkLXp1dWwtc2VjdXJpdHktc2VydmVyIiwic2NvcGUiOlsiV1JJR0hUIiwicmVhZCJdfQ.vOibf3j0seQqsJuH66eLi_zU_P3KeiTn07baUx78T5A <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> x-forwarded-host : localhost:5555 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> x-forwarded-proto : http <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> x-forwarded-prefix : /provider <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> host : localhost:5555 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> x-forwarded-port : 5555 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> x-forwarded-for : 0:0:0:0:0:0:0:1 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> accept-encoding : gzip <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> content-length : 0 <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>> connection : Keep-Alive <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>>>>>>> header end! <<<<<<<<<<<<<<<<<<<<<<
|
其中,authorization
就是 JWT Token,这个 Token 是使用 base64 加密的,将 authorization
去掉 bearer
后,其余部分按 “.” 分隔,每个部分分别解密
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
解码后为:
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
eyJleHAiOjE1NTA2MDcxODQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiV1JJR0hUX1dSSVRFIiwiV1JJR0hUX1JFQUQiXSwianRpIjoiZTJjYmNjNDktMzE5ZC00NDdhLTlmMWYtZmY0YzI5ZDFmZWM4IiwiY2xpZW50X2lkIjoic3ByaW5nLWNsb3VkLXp1dWwtc2VjdXJpdHktc2VydmVyIiwic2NvcGUiOlsiV1JJR0hUIiwicmVhZCJdfQ
解码后为:
1 2 3 4 5 6 7 8
| { "exp": 1550607184, "user_name": "admin", "authorities": ["WRIGHT_WRITE", "WRIGHT_READ"], "jti": "e2cbcc49-319d-447a-9f1f-ff4c29d1fec8", "client_id": "spring-cloud-zuul-security-server", "scope": ["WRIGHT", "read"] }
|
vOibf3j0seQqsJuH66eLi_zU_P3KeiTn07baUx78T5A:这一部分是密文,不能使用 base64 解密