Chapter 17 of in-depth understanding of Spring Cloud and microservice construction uses Spring Cloud OAuth2 to protect microservice systems

Chapter 17 of in-depth understanding of Spring Cloud and microservice construction uses Spring Cloud OAuth2 to protect microservice systems

1, What is OAuth2

OAuth2 is a standard authorization protocol. OAuth2 replaces the work of OAuth1 created in 2006. OAuth2 is not compatible with OAuth1, that is, OAuth1 is completely abandoned. OAuth2 allows different clients to access the protected resources through authentication and authorization. In the process of authentication and authorization, note that the following three roles are included

  • Service provider Authorization Server
  • Resource Server
  • Client client

Authentication process of OAuth2:

  1. The user (resource holder) opens the client, and the client asks for user authorization
  2. User consent authorization
  3. The client requests authorization from the authorization server
  4. The authorization server authenticates the client, including the authentication of user information. After the authentication is successful, the authorization is given to the token
  5. After the client obtains the token, it carries the token to request resources from the resource server
  6. The resource server confirms that the token is correct and releases the resource to the client

2, How to use Spring OAuth2

The implementation of OAuth2 protocol in Spring Resource is spring OAuth2. Spring OAuth2 is divided into two parts: OAuth2 Provider and OAuth2 Client. Let's explain them one by one

1.OAuth2 Provider

The OAuth2 Provider is responsible for disclosing the resources protected by OAuth2. The OAuth2 Provider needs to configure the OAuth2 client information on behalf of the user. The client allowed by the user can access the resources protected by OAuth2. The OAuth2 Provider controls whether the client has access to the resources protected by it by managing and verifying the OAuth2 token. In addition, OAuth2 Provider must also provide authentication API interface for users. According to the authentication API interface, the user provides information such as account and password to confirm whether the client can be authorized by OAuth2 Provider. The advantage of this is that the third-party client does not need to obtain the user's account and password, and can access the resources protected by OAuth2 through authorization

The roles of OAuth2 Provider are divided into Authorization Service and Resource Service. Usually, they are not in the same service. One Authorization Service may correspond to multiple resource services. Spring OAuth2 needs to be used together with Spring Security. All requests are processed by the Spring MVC controller and pass through a series of Spring Security filters

There are two nodes in the Spring Security filter chain, which obtain authentication and authorization from the Authorization Service

  • Authorization node: the default is / oauth/authorize
  • Get Token node: the default is / oauth/token

Authorization Server configuration

When configuring the Authorization Server, you need to consider the authorization type (such as authorization code, user credentials, refresh token) that the Client obtains the access token from the user. The Authorization Server needs to configure the details of the Client and the implementation of the token service

Add the @ EnableAuthorizationServer annotation to any class that implements the AuthorizationServerConfigurer interface, enable the function of the Authorization Server, inject it into the Spring IoC container in the form of Bean, and implement the following three configurations:

  • ClientDetailsServiceConfigurer: configure client information
  • AuthorizationServerEndpointsConfigurer: configure the nodes and Token services that authorize tokens
  • AuthorizationServerSecurityConfigurer: configure the security policy of the Token node

These three configurations are explained in detail below

(1)ClientDetailsServiceConfigurer
The configuration information of the client can be placed in memory or in the database. The following information needs to be configured:

  • clientId: client Id, which must be unique in the Authorization Server
  • secret: the password of the client
  • scope: the domain of the client
  • Authorized granttypes: authentication type
  • authorities: permission information

The client information can be stored in the database, so that the data of the client information can be updated in real time by changing the database. Spring OAuth2 has designed the tables of the database and is immutable. The script for creating the database is as follows:

-- ----------------------------
--  Table structure for `clientdetails`
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
  `appId` varchar(128) NOT NULL,
  `resourceIds` varchar(256) DEFAULT NULL,
  `appSecret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `grantTypes` varchar(256) DEFAULT NULL,
  `redirectUrl` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additionalInformation` varchar(4096) DEFAULT NULL,
  `autoApproveScopes` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_access_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` blob,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_approvals`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` datetime DEFAULT NULL,
  `lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_client_details`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_client_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_code`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
--  Table structure for `oauth_refresh_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(2)AuthorizationServerEndpointsConfigurer
By default, the authorization server endpoints configurator configuration enables all authentication types. Except for password type authentication, password authentication can only be enabled when the authentication manager is configured. The AuthorizationServerEndpointsConfigurer configuration consists of the following five items:

  • Authentication manager: password authentication is enabled only when this option is configured. In most cases, it is password authentication, so this option is generally configured
  • userDetailsService: configure the interface for obtaining user authentication information, which is similar to the userDetailsService implemented in the previous chapter
  • Authorization code services: configure the authentication code service
  • implicitGrantService: configure and manage the status of implicit authentication
  • tokenGranter: configure tokenGranter

In addition, you need to set the Token management policy. At present, the following three types are supported:

  • InMemoryTokenStore: the Token is stored in memory
  • JdbcTokenStore: tokens are stored in the database. You need to introduce the dependency package of spring JDBC, configure the data source, and initialize the database script of Spring OAuth2, that is, the database script in the previous section
  • JwtTokenStore: in the form of JWT, there is no storage, because JWT itself contains all the information verified by the user and does not need to be stored. In this form, the dependency of spring JWT needs to be introduced

(3)AuthorizationServerSecurityConfigurer
If the resource service and authorization service are in the same service, use the default configuration instead of any other configuration. However, if the resource service and authorization service are not in the same service, some additional configuration is required. If RemoteTokenServices (remote Token verification) is used, the Token carried by each request of the resource server needs to be verified from the authorization service. At this time, you need to configure the verification policy of the "/ oauth/check_token" verification node

The configuration of Authorization Server is complex and detailed. By adding the @ EnableAuthorizationServer annotation to the class that implements the AuthorizationServerConfigurer interface, the function of the Authorization Server is enabled and injected into the IoC container. Then you need to configure ClinetDetailsServiceConfigurer, AuthorizationServerSecurityConfigurer and authorizationserverendpoints configurer. They have many optional configurations that need readers to understand

Resource Server configuration

The Resource Server (which can be an authorization server or other resource services) provides OAuth2 protected resources, such as API interfaces, Html pages, Js files, etc. Spring OAuth2 provides a Spring Security authentication filter that implements this protection function. Add the @ EnableResourceServer annotation to the Configuration class annotated with @ Configuration to enable the function of Resource Server and configure it with resouceserverconfigurator (if necessary). The following contents need to be configured:

  • tokenServices: defines the Token Service. For example, use the ResourceServerTOkenservices class to configure how tokens are encoded and decoded. If the Resource Server and the Authorization Server are in the same project, you do not need to configure tokenServices. If they are not in the same program, you need to configure tokenServices. You can also use the RemoteTokenServices class, that is, the Resource Server uses the remote Authorization Server for Token decoding. At this time, this option does not need to be configured. This method is adopted in the cases in this chapter
  • resourceId: configuration resource Id

2.OAuth2 Client

The OAuth2 Client is used to access resources protected by OAuth2. The client needs to provide a mechanism for storing the user's authorization code and access token. The following two options need to be configured:

  • Protected Resource Configuration
  • Client configuration

Protected Resource Configuration

Use the Bean of OAuth2ProtectedResourceDetails type to define the protected resource. The protected resource has the following properties:

  • Id: the Id of the resource. It is not used in the Spring OAuth2 protocol. It is used by the client to find resources. It does not need to be configured. It can be used by default
  • clientId: the Id of OAuth2 Client, one-to-one correspondence with the previous configuration of OAuth2 Provider
  • clientSecret: client password, one-to-one correspondence with previous OAuth2 Provider configurations
  • accessTokenUri: the API node that gets the Token
  • scope: the domain of the client
  • clientAuthenticationScheme: there are two client authentication types: Http Basic and Form. The default is Http Basic
  • userAuthorizationUri: if the user needs to authorize access to resources, the user will be redirected to the authentication Url

Client Configuration

The @ EnableOAuth2Client annotation can be used to simplify the configuration of OAuth2 Client. In addition, the following two items need to be configured:

  • Create a filter Bean (the Id of the Bean is oauth2ClientContextFilter) to store the current request and the request of the context. During the request, if the user needs authentication, the user will be redirected to the authentication Url of OAuth2
  • Create a Bean of AccessTokenRequest type in the Request domain

3, Case analysis

In this case, there are three projects: Eureka server, auth service and service hi

First, the browser provides the auth service server with client information, user name and password, and requests to obtain a Token. After auth service confirms that the information is correct, it generates a Token according to the user's information and returns it to the browser. The browser needs to carry a Token to the resource service service hi every request in the future. After obtaining the Token carried by the request, the resource server sends the Token to the authorization service auth service for confirmation through remote scheduling. After the auth service confirms that the Token is correct, it returns the permission information of the user corresponding to the Token to the resource service service hi. If the user corresponding to the Token has permission to access the API interface, the request result will be returned normally, otherwise an error prompt of insufficient permission will be returned

1. Write Eureka Server

The whole project is in the form of Maven multi Module, and the Spring Boot version of the project is 2.1.0 and the Spring Cloud version is Greenwich.RELEASE

Under the main Maven project, create a moudle project of eureka server. After creation, introduce eureka server into the pom file of eureka server project. Since its inception, the code is as follows:

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

Configure Eureka Server in the configuration file application.yml of Eureka Server project, including configuring the port number of the program as 8761, the host as localhost, and non self registration. The specific configuration code is as follows:

server:
  port: 8761

eureka:
  instance:
    #Submit IP information
    prefer-ip-address: true
    hostname: localhost

  client:
    #By default, Eureka Server will register with itself. In this case, you need to configure register with Eureka and fetch registry to false to prevent yourself from registering
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone:
        http://${eureka.instance.hostname}:${server.port}/eureka/

Add @ EnableEurekaServer annotation on the startup class EurekaServer application of the program to enable the function of EurekaServer. The code is as follows:

package com.sisyphus;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

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

2. Prepare Uaa authorization services

Dependency management pom file

Create a Moudle project under the main Maven project, named auth service, as the Uaa service (authorization service), and introduce the dependencies required by the project in the pom file of auth service project. The code is as follows:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

Spring cloud starter security and spring security oauth2 autoconfigure dependencies need to be introduced. Mysql database is used in the project. The connection driver of MySQL depends on MySQL connector Java and JPA, and the start depends on spring boot starter data JPA. The WEB function is used in the project, and the start of WEB depends on spring boot starter WEB. As Eureka Client, this project introduces Eureka's starting dependency spring cloud starter Eureka

Configuration file application.yml

Make the following configuration in the project configuration file application.yml:

spring:
  application:
    name: service-auth
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8
    username: root
    password: 123456

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

server:
  servlet:
    context-path: /uaa
  port: 5000

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

Configure Spring Security

Because auth service needs to expose the API interface of check Token, auth service is also a resource service. Spring Security needs to be introduced into the project and configured to protect auth service resources. The configuration code is as follows:

package com.sisyphus.config;

import org.springframework.beans.factory.annotation.Autowired;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;

    @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(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());
    }

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

WebSecurityConfig class enables the WEB protection function through the @ EnableWebSecurity annotation, and enables the protection function on the method through the @ enableglobalmethodsecurity annotation. WebSecurityConfig class inherits WebSecurityConfigurerAdapter class and replicates the following three methods for related configuration:

  • configure(HttpSecurity http): all requests configured in HttpSecurity require security authentication
  • configure(AuthenticationManagerBuilder auth): the authenticated user information source and password encryption policy are configured in the AuthenticationManagerBuilder, and the AuthenticationManager object is injected into the IoC container. This needs to be configured in OAuth2, because authentication manager is configured in OAuth2 to enable password authentication. In this case, password authentication is used
  • authenticationManagerBean(): Bean with authentication management configured

UserServiceDetail implements the UserDetailsService interface and uses BCryPasswordEncoder to encrypt the password. The code is as follows:

package com.sisyphus.service;

import com.sisyphus.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserServiceDetail implements UserDetailsService {
    @Autowired
    private UserDao userRepository;


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

UserDao class inherits the JpaRepository. Write a method to obtain users according to user name in UserDao class. The code is as follows:

package com.sisyphus.dao;

import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

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

The User class needs to implement the UserDetails interface, and the Role class needs to implement the GrantedAuthority interface. The code is as follows:

package com.sisyphus.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

@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;
    }
}
package com.sisyphus.entity;

import org.springframework.security.core.GrantedAuthority;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@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 void setName(String name) {
        this.name = name;
    }

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

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

Configure Authorization Server

First list the configuration codes, and then describe each configuration in detail according to the codes. The codes are as follows:

package com.sisyphus;

import com.sisyphus.service.UserServiceDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
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.configuration.EnableResourceServer;
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.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@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 {
        
        JdbcTokenStore tokenStore = new JdbcTokenStore(dataSource);

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

        @Autowired
        private UserServiceDetail userServiceDetail;

        @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(tokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userServiceDetail);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()")
                    .allowFormAuthenticationForClients()
                    .passwordEncoder(NoOpPasswordEncoder.getInstance());
        }
    }
}

Add @ EnableEurekaClient annotation to the program startup class AuthServiceApplication to enable the function of Eureka Client, and add @ EnableResourceServer annotation to enable Resource Server. The program needs to expose the API interface for obtaining Token and the API interface for verifying Token, so the program is also a resource service

The OAuth2AuthorizationConfig class inherits the AuthorizationServerConfigurerAdapter and adds the annotation @ EnableAuthorizationServer on this class to enable the authorization service. As an authorization service, you need to configure three options: ClientDetailsServiceConfigurer, AuthorizationServerEndpointsConfigurer, and AuthorizationServerSecurityConfigurer

The ClientDetailsServiceConfigurer configures some basic information of the client. The clients.inMemory() method configures to store the information of the client in memory. The. withClient("browser") method creates a client with clientId as browser, and the authorized grant types ("refresh_token", "password") method configures the authentication type as refresh_ Token and password, the. scope("ui") method configures the client domain as "ui". Then another client is created with Id "service Hi"

AuthorizationServerEndpointsConfigurer needs to configure tokenStore, AuthenticationManager and userServiceDetail. Among them, tokenStore (Token storage method) stores tokens in memory, that is, InMemoryTokenStore is used. If the resource service and authorization service are the same service, InMemoryTokenStore is the best choice. If the resource service and authorization service are not the same service, the InMemoryTokenStore is not used to store tokens. Because when the authorization service fails, the service needs to be restarted, and all the tokens in the existing memory are lost, resulting in the failure of all the tokens of the resource service. Another way is to use JdbcTokenStore, that is, use the database to store. Using JdbcTokenStore to store requires the introduction of connection database dependencies, such as MySQL connector and JPA in this example, and the database needs to be initialized. AuthenticationManager needs to configure the Bean of AuthenticationManager, which comes from the configuration in websecurityconfigureradapter. Only when this Bean is configured can password type authentication be enabled. Finally, userDetailsService is configured to read the information of authenticated users

The AuthorizationServerSecurityConfigurer configures the policy of obtaining the Token. In this case, the request for obtaining the Token is not intercepted. Only the verification information of the obtained Token needs to be verified. If the information is accurate, the Token is returned. In addition, the policy of checking Token is configured

Expose Remote Token Services interface

This case uses RemoteTokenServices to verify the Token. If other resource services need to verify the Token, they need to remotely call the API interface for verifying the Token exposed by the authorization service. In this case, the API interface code for verifying Token is as follows:

package com.sisyphus.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

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

3. Write service hi resource service

Project dependency

Under the main Maven project, create a Moudle project named service hi as a resource service. Import the dependencies required by the project in the pom file of the service hi project. The code is as follows:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</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-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.0.0.RELEASE</version>
    </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 database is used in the project and the ORM framework of JPA is used to operate the database. Therefore, it is necessary to introduce JPA into the pom file of the project. The start of JPA depends on spring boot starter data JPA and MySQL database connector depends on MySQL connector Java. As Eureka Client, Eureka needs to be introduced into the pom file of the project. The start of Eureka depends on spring cloud starter Netflix Eureka Client. As a WEB server, it is necessary to introduce the start of WEB into the pom file of the project, which depends on spring boot starter WEB. In addition, when Feign is used as the remote scheduling framework, it is necessary to introduce Feign's start dependency spring cloud starter security and spring security oauth2 autoconfigure in the pom file of the project

Configuration file application.yml

Configure the program in the project configuration file application.yml. The specific configuration is as follows:

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

server:
  port: 8762

spring:
  application:
    name: service-hi
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  main:
    allow-bean-definition-overriding: true

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

In the above configuration file, the program name is service Hi, the port is 8762, and the address of the service registry is http://localhost:8761/eureka/ , configure the database connection driver, database connection address, database user name and password, as well as the related configuration of JPA. Then, configure security.oauth2.resource, specify the address of user info URI, which is used to obtain the user information of the current Token, configure the relevant information of security.oauth2.client, clientId, clientSecret and other information. These configurations need to correspond to those configured in the Uaa service

Configure Resource Server

As a Resource Server, the service hi project needs to configure the relevant configuration of the Resource Server. The configuration code is as follows:

package com.sisyphus.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;

@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();
    }
}

Add the @ EnableResourceServer annotation on the resourceserverconfigurator class to enable the function of Resource Server, and add the @ EnableGlobalMethodSecurity annotation to enable method level protection. The ResourceServerConfigurer class inherits the ResourceServerConfigurerAdapter class, overrides the configure(HttpSecurity http) method, and configures which requests need to be verified and which requests do not need to be verified through ant expression. For example, in this case, the interface of "/ user/register" does not need to be verified, and all other requests need to be verified

Configure OAuth2 Client

OAuth2 Client is used to access resources protected by OAuth2. As OAuth2 Client, service hi has the following configuration code:

package com.sisyphus.config;

import feign.RequestInterceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;

@Configuration
@EnableConfigurationProperties
@EnableOAuth2Client
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());
    }
}

To put it simply, three options need to be configured: one is to configure the information of protected resources, that is, ClientCredentialsResourceDetails; Second, configure a filter to store the current Request and context; Third, create a Bean of AccessTokenRequest type in the Request field

Now use the above code to specify. Add the @ EnableOAuth2Client annotation on the OAuth2ClientConfig class to enable the function of the OAuth2 Client; A Bean of ClientCredentialsResourceDetails type is configured. The Bean obtains the configuration properties of the Bean by reading the configuration prefixed with security.oauth2.client in the configuration file; Inject a Bean of OAuth2FeignRequestInterceptor type filter; Finally, a Bean of OAuth2RestTemplate type is injected to request from the Uaa service

So far, the authorization service, resource service and OAuth2 client have been built. Now write a registered API interface for testing

Write user registration interface

First, write a User class. In this case, JPA is used as the ORM framework. JPA annotation needs to be added to the User class, which is the same as the User class of Uaa service. The code is as follows:

package com.sisyphus.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

@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;
    }
}

The data operation class UserDao inherits the JpaRepository. UserDao has the basic method of operating database single tables. The code is as follows:

package com.sisyphus.dao;

import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

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

The UserServiceImpl class of the Service layer contains a method for creating user logic, in which BCryptPasswordEncoder class is used to encrypt passwords. The code is as follows:

package com.sisyphus.service;

import com.sisyphus.dao.UserDao;
import com.sisyphus.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Service
public class UserServiceImpl{
    private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    
    @Autowired
    private UserDao userDao;
    
    public User create(String username, String password){
        User user = new User();
        user.setUsername(username);
        String hash = encoder.encode(password);
        user.setPassword(hash);
        User u = userDao.save(user);
        return u;
    }
}

Write the UserController class. There is a registered API interface in the class. The code is as follows:

package com.sisyphus.controller;

import com.sisyphus.entity.User;
import com.sisyphus.service.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserServiceImpl userService;
    
    @RequestMapping(value = "/registry", method = RequestMethod.POST)
    public User createUser(@RequestParam("username") String username, @RequestParam("password") String password){
        return userService.create(username, password);
    }
}

Write a test class HiController, which has three interfaces: the first API interface "/ hi", which does not require any permissions. You only need to verify whether the Token in the Header is correct, and the Token can be accessed; The second API interface "/ hello" requires "ROLE_ADMIN" permission; The third interface "/ getPrinciple" enables the user to obtain the current Token user information. The code is as follows:

package com.sisyphus.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@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("hasAuthority('ROLE_ADMIN')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello!";
    }
    
    @GetMapping("/getPrinciple")
    public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication){
        logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
        logger.info("principal.toString()" + principal.toString());
        logger.info("principal.getName" + principal.getName());
        logger.info("authentication:" + authentication.getAuthorities().toString());
        return oAuth2Authentication;
    }
}

Let's demonstrate the whole process
(1) Simulate the request through the Curl command, call the registered API interface and register a user. The Curl command is as follows:

curl -d "username=sisyphus&password=123456" "localhost:8762/user/registry"

The registration is successful, and the returned results are as follows:

(2) Simulate the request through the Curl command and call the API interface to obtain the Token. The Curl command is as follows:

curl -i -X POST -d "username=sisyphus&password=123456&grant_type=password&client_id=service-hi&client_secret=123456" http://localhost:5000/uaa/oauth/token

The Token is obtained successfully. The returned results are as follows:

(3) Simulate the request through the Curl command to access the interface "/ hi" that does not require permission points. The Curl command is as follows:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hi"

The returned results are as follows:

(4) Simulate the request through the Curl command and access the API interface "/ hello" that requires a "ROLE_ADMIN" permission point. The Curl command is as follows:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hello"

Because the user does not have the "ROLE_ADMIN" permission point, he does not have permission to access the API interface. The returned results are as follows:

(5) Give the user "ROLE_ADMIN" permission in the database and execute the following script in the database:

INSERT INTO role VALUES ('1','ROLE_USER'),('2','ROLE_ADMIN');
INSERT INTO user_role VALUES ('1', '2');

(6) After giving the user "ROLE_ADMIN" permission, re access the API interface "/ hello", and the Curl command is as follows:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hello"

The returned results are as follows:

It can be seen from the results returned by the above request that after adding the "ROLE_ADMIN" permission to the user, the request can obtain the normal return results. It can be seen that the resource service protected by Spring Cloud OAuth2 needs to verify the requested user information and the permissions of the user. If the verification passes, the correct result will be returned, otherwise the result of "no access" will be returned

4, Summary

The defect of this architecture is that each request requires the internal remote scheduling auth service of the resource service to verify the correctness of the Token and the permissions of the user corresponding to the Token, resulting in an additional internal request. In the case of high concurrency, auth service needs cluster deployment and caching

Tags: Spring Cloud Microservices

Posted on Fri, 03 Dec 2021 06:26:51 -0500 by behicthebuilder