Spring Cloud需要使用OAUTH2来实现多个微服务的统一认证授权,通过向OAUTH服务发送某个类型的grant type进行集中认证和授权,从而获得access_token,而这个token是受其他微服务信任的,我们在后续的访问可以通过access_token来进行,从而实现了微服务的统一认证授权。

整体架构

验证流程

 

一般的使用流程:1、用户通过登陆请求致auth-server来通过用户信息获取Token,在之后的请求中使用Token,2、在请求致其他需要权限微服务时,服务会根据Token来获取用户信息【包含权限】来鉴权校验,3、需要权限微服务需要制定安全规则,标明每个方法所需权限,同时指明获取用户信息位置【auth-server】。

  • auth-server:OAUTH2认证授权中心
  • order-service:普通微服务,用来验证认证和授权
  • api-gateway:边界网关(所有微服务都在它之后)

OAUTH2中的角色:

  • Resource Server:被授权访问的资源
  • Authotization Server:OAUTH2认证授权中心
  • Resource Owner: 用户
  • Client:使用API的客户端(如Android 、IOS、web app)

Grant Type:

  • Authorization Code:用在服务端应用之间
  • Implicit:用在移动app或者web app(这些app是在用户的设备上的,如在手机上调起微信来进行认证授权)
  • Resource Owner Password Credentials(password):应用直接都是受信任的(都是由一家公司开发的,本例子使用)
  • Client Credentials:用在应用API访问。

1.基础环境

使用MongoDB作为账户存储,Redis作为Token存储。

项目依赖

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

<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>${fastjson.version}</version>
</dependency>

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

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-eureka-server</artifactId>
 <version>1.4.0.RELEASE</version>
</dependency>

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-feign</artifactId>
 <version>1.4.0.RELEASE</version>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-devtools</artifactId>
 <optional>true</optional>
</dependency>

<dependency>
 <groupId>org.springframework.retry</groupId>
 <artifactId>spring-retry</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-bus-amqp</artifactId>
 <version>1.3.2.RELEASE</version>
</dependency>

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

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-config</artifactId>
 <version>1.4.0.RELEASE</version>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-mongodb</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>

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

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

<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>




 

 

2.auth-server

2.1 OAuth2服务配置

Redis用来存储token,服务重启后,无需重新获取token.

 

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 //认证管理器
 @Autowired
 AuthenticationManager authenticationManager;
 //存储链接
 @Autowired
 RedisConnectionFactory redisConnectionFactory;
 //用户信息相关的实现
 @Autowired
 private UserDetailsService userDetailsService;

 //token存放位置
 @Bean
 public RedisTokenStore tokenStore() {
 return new RedisTokenStore(redisConnectionFactory);
 }


 //配置认证管理器以及用户信息业务实现
 @Override
 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
 endpoints
 .authenticationManager(authenticationManager)
 .userDetailsService(userDetailsService)//若无,refresh_token会有UserDetailsService is required错误
 .tokenStore(tokenStore());
 }
 //配置认证规则,那些需要认证那些不需要
 @Override
 public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
 security
 .tokenKeyAccess("permitAll()")
 .checkTokenAccess("isAuthenticated()");
 }
 //配置客户端
 @Override
 public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
 clients.inMemory()
 .withClient("android")//客户端账户
 .scopes("xx")//作用域
 .secret("android")//客户端密码
 .authorizedGrantTypes("password", "authorization_code", "refresh_token")//授权方式
 .and()//不同的客户端链接
 .withClient("webapp")
 .scopes("xx")
 .authorizedGrantTypes("implicit");
 }



 

 

2.2 Resource服务配置

auth-server提供user信息,所以auth-server也是一个Resource Server

package com.yangcuncloud.consumer.annotation;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

import javax.servlet.http.HttpServletResponse;

/**
 * Resource Server
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig
        extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
}


提供用户信息以供鉴权

package com.yangcuncloud.consumer.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;

import java.security.Principal;

/**
 * 通过token获取用户信息
 */
@RestController
public class UserController {

    @GetMapping("/user")
    public Principal user(Principal user){
        return user;
    }
}


 

2.3 安全配置

package com.yangcuncloud.consumer.annotation;

import com.yangcuncloud.consumer.security.DomainUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.data.repository.query.SecurityEvaluationContextExtension;

/**
 * 安全配置相关
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
/**
*
*注意: @EnableGlobalMethodSecurity 可以配置多个参数:
*prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]    此处表明可用
*secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
*jsr250Enabled :决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
*/

public class SecurityConfig extends WebSecurityConfigurerAdapter {


    //用户信息业务类
    @Bean
    public UserDetailsService userDetailsService() {
        return new DomainUserDetailsService();
    }
    //密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //验证用户信息与密码
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
        return new SecurityEvaluationContextExtension();
    }

    //不定义没有password grant_type
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


}


 

 

2.4 权限设计

采用用户(SysUser)<->角色(SysRole)<->权限(SysAuthotity)设置,彼此之间的关系是多对多。通过DomainUserDetailsService 加载用户和权限。

2.5 配置

 
server:
  port: 8058

spring:
  application:
    name: consumer
  cloud:
    config:
      label: master
      profile: dev
      discovery:
        enabled: true
        service-id: config
    bus:
      enabled: true
  redis:
    host: 172.16.20.19
    Port: 6379

management:
  security:
    enabled: false

eureka:
  client:
    serviceUrl:
      defaultZone: http://172.16.20.19:8051/eureka/


logging.level.org.springframework.security: DEBUG

logging.leve.org.springframework: DEBUG

security:
  oauth2:
    resource:
      filter-order: 3

 

3.Resource

3.1 Resource服务配置,服务有需要受保护接口时,需要添加以下配置

package com.yangcuncloud.consumer.annotation;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

import javax.servlet.http.HttpServletResponse;

/**
 * Resource Server
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig
        extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
}


 

3.2 用户信息配置

微服务使用auth-server进行认证授权,在它的配置文件指定用户信息在auth-server的地址即可:

security:
  oauth2:
    resource:
	  id: 服务id
      user-info-uri: http://验证服务ip:port/用户信息接口
      prefer-token-info: false

 

security: oauth2: resource: id: order-service user-info-uri: http://auth-serverHOST:port/用户信息接口 prefer-token-info: false

3.3 权限测试控制器

在需要鉴权的接口上添加注解

@PreAuthorize("hasAuthority('query-demo')")

此处的‘query-demo’就是对应的权限名

@PreAuthorize("hasAuthority('query-demo')")
@RequestMapping(value = "/userList")//此注解标明调用该接口的TOken用户需要拥有query-demo的权限
public ResponseEntity<JSONObject> userList(@RequestParam(value = "page") int page,
                                           @RequestParam(value = "pageSzie") int pageSzie,
                                           @RequestParam(value = "param", required = false) String param,
                                           @RequestParam(value = "order", required = false) String sort) {


具备authority的query-demo权限的才能访问

演示

 客户端调用

使用Postmanhttp://localhost:8080/consumer/oauth/token发送请求获得access_token


注意此出需要添加authorization header,为我们之前的客户端账户和客户端密码(都为android)

 


之后我们使用获取的Token来验证鉴权是否有效,先使用无Token和错误Token请求

 

可以看到都是不可访问的情况,然后我们来尝试使用正确Token调用

访问成功!

然后我们修改该接口的权限,并重启服务,可以看到重启后原Token依然可用,因为我们将其保存至Redis中了。此时我们看一下该接口是否依然可用。【权限错误的情况】

再次请求

显示不允许访问。

6 注销oauth2

6.1 增加自定义注销Endpoint

所谓注销只需将access_tokenrefresh_token失效即可,我们模仿org.springframework.security.oauth2.provider.endpoint.TokenEndpoint写一个使access_tokenrefresh_token失效的Endpoint:

@FrameworkEndpoint
public class RevokeTokenEndpoint {

    @Autowired
    @Qualifier("consumerTokenServices")
    ConsumerTokenServices consumerTokenServices;

    @RequestMapping(method = RequestMethod.DELETE, value = "/oauth/token")
    @ResponseBody
    public String revokeToken(String access_token) {
        if (consumerTokenServices.revokeToken(access_token)){
            return "注销成功";
        }else{
            return "注销失败";
        }
    }
} 

 

 

6.2 注销请求方式

在注销之后使用原token再次访问就会错误invalid_tokenInvalid access token: 需要重新获取token。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐