SpringCloudOAuth2案例

本文详细解析了在微服务架构中,如何利用OAuth2实现服务间及客户端的安全认证。介绍了服务注册中心EurekaServer、授权中心Uaa(auth-service)与资源服务(service-hi)之间的交互过程,包括Token的生成、验证及权限检查。
案例分析

首先来看案例的架构设计,在这个案例中有3个工程,分别是服务注册中心工程eureka-server、授权中心Uaa工程auth-service和资源工程service-hi,如图:

在这里插入图片描述
首先,浏览器向auth-service 服务器提供客户端信息、用户名和密码,请求获取Token。auth-service确认这些信息无误后,根据该用户的信息生成Token并返回给浏览器。浏览器在以后的每次请求都需要携带Token给资源服务service-hi,资源服务器获取到请求携带的Token后,通过远程调度将Token给授权服务auth-service确认。auth-service确认Token正确无误后,将该Token对应的用户的权限信息返回给资源服务service-hi。如果该Token对应的用户具有访问该API接口的权限,就正常返回请求结果,否则返回权限不足的错误提示。

编写Eureka Sever:
依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
</dependencies>

配置:

server:
  port: 8761

security:
  user:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

启动类:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

编写Uaa授权服务:

在主Maven工程下创建-一个Module工程,取名为auth-service, 作为Uaa服务(授权服务),在auth-service工程的pom文件里引入工程所需的依赖,代码如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
</dependencies>

其中,spring-cloud-starter-oauth2 是对spring-cloud-starter-security、 spring- security-oauth2和spring-security-jwt这3个起步依赖的整合。在工程中使用了MySQL数据库,引入了MySQL的连接驱动依赖mysql-connector-java和JPA的起步依赖spring-boot- starter- data-jpa。在工程中使用了Web功能,引入了Web的起步依赖spring -boot starter- web。这个工程作为Eureka Client,引入了Eureka 的起步依赖spring-cloud-starter-eurekao

配置:

server:
  port: 5000
  context-path: /uaa
spring:
  application:
    name: service-auth
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

security:
  oauth2:
    resource:
      filter-order: 3

eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka/

在上面的配置中,配置了程序名为service -auth,程序的端口号为5000, context-path为“/uaa";配置了MySQL 数据库的相关配置,包括数据源、用户和密码,其中数据库名为spring-cloud-auth,需要初始化12.3.1节的数据库脚本;使用JPA作为ORM框架,并对JPA做了相关的配置;配置了服务注册中心的地址为htp://ocalhost:8761/eureka/; 配置security.oauth2 resource.filter-order为3,在Spring Boot 1.5.x版本,这是固定写法,在Spring Boot 1.5.x版本之前,默认即可。

配置Spring Security

由于auth-service需要对外暴露检查Token的API接口,所以auth-service也是一个资源服务,需要在工程中引入Spring Security,并做相关的配置,对auth-service资源进行保护。配置代码如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

WebScurityConfig类通过@EnableWebSecurity注解开启Web保护功能,通过@EnableGlobalMethodSecurity注解开启在方法上的保护功能。WebSecurityConfig 类继承了WebSecurity-ConfigurerAdapter类,并复写了以下3个方法来做相关的配置.

  • configure(HttpSecurity http):HttpSecurity 中配置了所有的请求都需要安全验证
  • configure(AuthenticationManagerBuilder auth);:AuthenticationManagerBuilder 中配置了验证的用户信息源和密码加密的策略,并且向IoC容器注入AuthenticationManager对象。这需要在OAuth2中配置,因为在OAuth2中配置了AuthenticationManager,密码验证才会开启。在本例中,采用的是密码验证。
  • authenticationManagerBean():配置了验证管理的Bean。

UserService:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserDao userRepository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return userRepository.findByUsername(s);
    }
}

UserDao:

public interface UserDao extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

User:

@Entity
public class User implements UserDetails, Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private List<Role> authorities;

    public User() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Role:

@Entity
public class Role implements GrantedAuthority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getAuthority() {
        return name;
    }
}

配置Authorization Server:

@SpringBootApplication
@EnableResourceServer
@EnableEurekaClient
public class AuthServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }

    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;

    @Configuration
    @EnableAuthorizationServer
    protected class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
        // private TokenStore tokenStore = new InMemoryTokenStore();
        JdbcTokenStore jdbcTokenStore = new JdbcTokenStore(dataSource);

        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;

        @Autowired
        private UserService userService;

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("browser")
                    .authorizedGrantTypes("refresh_token", "password")
                    .scopes("ui")
                    .and()
                    .withClient("service-hi")
                    .secret("123456")
                    .authorizedGrantTypes("client_credentials", "refresh_token", "password")
                    .scopes("server");
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                    .tokenStore(jdbcTokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userService);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }
    }

}

在程序启动类ServiceAuthApplication加上@EnableEurekaClient注解,开启Eureka Client客户端的功能,加上@EnableResourceServer注解,开启Resource Server。 程序需要对外暴露获取Token的API接口和验证Token的API接口,所以该程序也是一个资源服务。

OAuth2AuthorizationConfig类继承AuthorizationServerConfigurerAdapter,并在这个类上加上注解@EnableAuthorizationServer, 开启授权服务的功能。作为授权服务需要配置3个选项,分别为ClientDetailsServiceConfigurer、AuthorizationServerEndpointsConfigurer 和AuthorizationServerSecurityConfigurer。

其中,ClientDetaisServiceConfigurer 配置了客户端的一些基本信息,clients.inMemory()方法配置了将客户端的信息存储在内存中,.withClient("browser")方法创建了一个clientld 为browser 的客户端,authorizedGrantTypes("refresh token", "password")方法配置了验证类型为refresh token和password, .scopesl("ui")方法配置了客户端域为“ui”。接着创建了另一个client,它的Id为“service-hi"。

AuthorizationServerEndpointsConfigurer需要配置tokenStore 、authenticationManager 和userServiceDetail其中,tokenStore (Token 的存储方式)采用的方式是将Token存储在内存中,即使用InMemory TokenStore。如果资源服务和授权服务是同一个服务,用InMemory TokenStore是最好的选择。如果资源服务和授权服务不是同一个服务, 则不用InMemoryTokenStore 进行存储Token。因为当授权服务出现故障,需要重启服务,之前存在内存中Token全部丢失,导致资源服务的Token全部失效。

另外一种方式是用JdbcTokenStore,即使用数据库去存储,使用Jdbc TokenStore存储需要引入连接数据库依赖,如本例中的MySQL连接器、JPA, 并且需要初始化数据库脚本。authenticationManager 需要配置AuthenticationManager这个Bean,这个Bean来源于WebSecurityConfigurerAdapter中的配置,只有配置了这个Bean才会开启密码类型的验证。最后配置了userDetailService, 用来读取验证用户的信息。

AuthorizationServerSecurityConfigurer配置了获取Token的策略,在本案例中对获取Token请求不进行拦截,只需要验证获取Token的验证信息,这些信息准确无误,就返回Token。另外配置了检查Token的策略。

暴露Remote Token Services接口:

本案例采用RemoteTokenServices这种方式对Token进行验证。如果其他资源服务需要验证Token,则需要远程调用授权服务暴露的验证Token的API接口。本案例中验证Token的API接口的代码如下:

@RestController
@RequestMapping("/users")
public class UserController {

    @RequestMapping(value = "/current", method = RequestMethod.GET)
    public Principal getUser(Principal principal) {
        return principal;
    }
}

获取Token:

前提:数据库中初始化了token的sql脚本:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
postman:
在这里插入图片描述

编写service-hi资源服务:

在主Maven工程下,创建一一个 Module工程,取名为service-hi,这个工程作为资源服务。在service-hi工程的pom文件引入项目所需的依赖,代码如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

在工程中用到了MySQL数据库,采用JPA的ORM框架来操作数据库,所以需要在工程的pom文件引入JPA的起步依赖spring-boot-starter-data-jpa和MySQL数据库连接器依赖mysql-connector-java。作为Eureka Client, 需要在工程的pom文件引入Eureka 的起步依赖spring-cloud-starter-eureka。作为Web服务器,需要在工程的pom文件引入Web的起步依赖spring-boot-starter-web。另外使用Feign 作为远程调度框架,需要在工程的pom文件引入Feign的起步依赖spring-cloud-starter-feign。 最后作为资源服务器,需要在工程的pom文件引入OAuth2的起步依赖spring-cloud-starter- oauth2。

配置文件:application.yml

server:
  port: 8762

spring:
  application:
    name: service-hi
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:5000/uaa/users/current
    client:
      client-id: service-hi
      client-secret: 123456
      access-token-uri: http://localhost:5000/uaa/oauth/token
      grant-type: client_credentials,password
      scope: server

eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka/

配置了security.oauth2.resource, 指定了user-info-uri的地址,用于获取当前Token的用户信息,配置了security.oauth2.client 的相关信息,以及clientId、clientSecret等信息,这些配置需要和在Uaa服务中配置的一一对应。

service-hi工程作为Resource Server (资源服务),需要配置Resource Server的相关配置,配置代码如下:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/registry").permitAll()
                .anyRequest().authenticated();
    }
}

在ResourceServerConfigurer类上加@EnableResourceServer 注解,开启Resource Server的功能,加@EnableGlobalMethodSecurity 注解,开启方法级别的保护。ResourceServerConfigurer 类继承ResourceServerConfigurerAdapter 类,并重写configre(HttpSecurity http)方法,通过ant表达式,配置哪些请求需要验证,哪些请求不需要验证。如本案例中“/useregister”的接口不需要验证,其他所有的请求都需要验证。

配置OAuth Client:
OAuth2 Client 用来访问被OAuth2保护的资源。service-hi 作为OAuth2 Client, 它的配置代码如下:

@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }
    
    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor() {
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
                clientCredentialsResourceDetails());
    }
    
    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }
}

需要配置3个选项:

  • 一是配置受保护的资源的信息,即ClientCredentialsResourceDetails;
  • 二是配置一个过滤器,存储当前请求和上下文;
  • 三是在Request域内创建AccessTokenRequest类型的Bean。

现在通过上述代码来具体说明,在OAuth2ClientConfig类上加@EnableOAuth2Client注解,开启OAuth2 Client 的功能;并配置了一个ClientCredentialsResourceDetails 类型的Bean, 该Bean是通过读取配置文件中前缀为security.oauth2.client的配置来获取Bean的配置属性的;注入一个OAuth2FeignRequestInterceptor类型过滤器的Bean;最后注入了一个用于向Uaa服务请求的OAuth2RestTemplate类型的Bean。

到目前为止,授权服务、资源服务和0Auth2客户端都已经搭建完毕,现在写一个注册API接口来做测试。

编写用户注册接口:

首先编写一个User类,在本案例总共采用了JPA作为ORM框架,需要在User类加上JPA的注解,同Uaa服务的User类一样。

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column
    private String password;
	......
}

数据操作类UserDao继承了JpaRepository, UserDao具备了基本的操作数据库单表的基本方法,代码如下:

public interface UserDao extends JpaRepository<User, Long> {
}

Service层的UserServicelmpl类包含-一个创建用户逻辑的方法, 其中用到BCryptPasswordEncoder类来加密密码,代码如下:

@Service
public class UserServiceImpl implements UserService{
    
    private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    
    @Autowired
    private UserDao userDao;
    
    @Override
    public User create(User user) {
        String hash = encoder.encode(user.getPassword());
        user.setPassword(hash);
        User u = userDao.save(user);
        return u;
    }
}

编写UserController类,在类中有一个注册的API接口,代码如下:

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @PostMapping("/registry")
    public User createUser(@RequestParam("username") String username, @RequestParam("password") String password) {
        User u = new User();
        u.setPassword(password);
        u.setUsername(username);
        return userService.create(u);
    }
}

编写一个测试类HiController,其中有3个接口:第一个API接口“hi”,不需要任何权限,只需要验证Header中的Token正确与否,Token 正确即可访问;第二个API接口“hello",需要“ROLE ADMIN"权限;第三个接口“/getPrinciple”,, 用户获取当前Token 用户信息。代码如下:

@RestController
public class HiController {
    Logger logger = LoggerFactory.getLogger(HiController.class);
    
    @Value("${server.port}")
    String port;
    
    @RequestMapping("/hi")
    public String home() {
        return "hi :" + ", i am from port:" + port;
    }
    
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    @RequestMapping("/hello")
    public String hello() {
        return "hello you!";
    }
    
    @GetMapping("/getPriciple")
    public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication,
                                             Principal principal, Authentication authentication) {
        logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
        logger.info(oAuth2Authentication.toString());
        logger.info("principal.toString()" + principal.toString());
        logger.info("principal.getName()" + principal.getName());
        logger.info("authentication:" + authentication.getAuthorities().toString());
        return oAuth2Authentication;
    }
    
}
测试

调用注册API接口,注册一个用户:
在这里插入图片描述
调用获取Token的API接口:
在这里插入图片描述
访问不需要权限点的接口"/hi":
在这里插入图片描述

访问需要有权限"ROLE_ADMIN"权限点的API接口"/hello":
在这里插入图片描述
在数据库中给予用户"ROLE_ADMIN"权限:

insert into `role` values('1', 'ROLE_USER'),('2','ROLE_ADMIN');
insert into `user_role` values('3','2');

再次访问:
在这里插入图片描述

访问"/getPriciple"接口:
在这里插入图片描述

总结

案例的架构有改进之处,例如在资源服务器加一个登录接口,该接口不受Spring Security保护。登录成功后,service-hi 远程调用auth-service 获取Token返回给浏览器,浏览器以后所有的请求都需要携带该Token。
这个架构存在的缺陷就是每次请求都需要资源服务内部远程调度auth-service服务来验证Token的正确性,以及该Token对应的用户所具有的权限,额外多了一次内部请求。如果在高并发的情况下, auth-service需要集群部署,并且需要做缓存处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值