Implementing distributed system authorization with Spring Security

1 demand analysis

The technical solutions reviewed are as follows:

1. The UAA authentication service is responsible for authentication authorization.

2. All requests arrive at the microservice through the gateway

3. The gateway is responsible for authenticating the client and forwarding the request

4. The gateway parses the token and sends it to the micro service for authorization.

2 Registration Center

All micro service requests go through the gateway. The gateway reads the address of the micro service from the registry and forwards the request to the micro service.

This section completes the construction of the registration center, which adopts Eureka.

1. Create maven project

2,pom.xml dependencies are as follows

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-
4.0.0.xsd">
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.lw.security</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>distributed-security-discovery</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
</project>

3. Configuration file

Configure application in resources yml

spring: 
    application:
        name: distributed-discovery
server:
    port: 53000 #Boot port
eureka:
  server:
    enable-self-preservation: false    #Turn off the self-protection of the server. If the error reaches 80% within 15 minutes after the heartbeat detection of the client, the service will be protected, causing others to think it is a good service
    eviction-interval-timer-in-ms: 10000 #Cleaning interval (unit: ms, default: 60)*1000)5 Seconds to remove the services rejected by the client from the service registration list# 
    shouldUseReadOnlyResponseCache: true #eureka is an AP based strategy based on CAP theory. In order to ensure strong consistency, the CP does not turn off by default, and false turns off
  client: 
    register-with-eureka: false  #false: do not register with the registry as a client
    fetch-registry: false      #When it is true, it can be started, but an exception is reported: Cannot execute request on any known server
    instance-info-replication-interval-seconds: 10 
    serviceUrl: 
      defaultZone: http://localhost:${server.port}/eureka/
  instance:
    hostname: ${spring.cloud.client.ip-address}
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}

Startup class:

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

3 gateway

Gateway integration oauth2 0 has two ideas. One is that the authentication server generates jwt token, and all requests are verified and judged in the gateway layer; The other is handled by each resource service, and the gateway only forwards requests.

We choose the first one. We use the API gateway as oauth2 0, which realizes access client permission interception, token parsing and forwarding the current login user information (jsonToken) to the micro service, so that the downstream micro service does not need to care about token format parsing and oauth2 0 related mechanism.

API gateway is mainly responsible for two things in the authentication and authorization system:

(1) As oauth2 0's resource server role to intercept the access Party's permission.

(2) The token parses and forwards the current login user information (clear text token) to the microservice

After getting the plaintext token (the plaintext token contains the identity and permission information of the login user), the micro service also needs to do two things:

(1) User authorization interception (see whether the current user has access to the resource)

(2) Store the user information into the current thread context (which is conducive to the subsequent business logic to obtain the current user information at any time)

3.1 create project

1,pom.xml

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-
4.0.0.xsd">
    <parent>
        <artifactId>distributed-security</artifactId>
        <groupId>com.lw.security</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>distributed-security-gateway</artifactId>
    <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-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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-netflix-zuul</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-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

2. Configuration file

Configure application properties

spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true
logging.level.root = info
logging.level.org.springframework = info
zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *
zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**
zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

Both unified authentication service (UAA) and unified user service are micro services under the gateway. It is necessary to add routing configuration on the gateway:

zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**

zuul.routes.user-service.stripPrefix = false
zuul.routes.user-service.path = /order/**

It is configured above that if the request url received by the gateway conforms to the / order / * * expression, it will be forwarded to order service (Unified User Service).

Startup class:

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

3.2 token configuration

As mentioned earlier, because the resource server needs to verify and parse tokens, it can often expose check in the authorization server_ The Endpoint of the token is used, and we use the symmetric encrypted jwt in the authorization server, so we can know the key. The resource service and authorization service are designed symmetrically, so we can copy the two classes of TokenConfig of the authorization service.

@Configuration
public class TokenConfig {
  private String SIGNING_KEY = "uaa123";
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter()); 
    }
   @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //Symmetric secret key, which is used by the resource server to decrypt
        return converter;
    }
}

3.3 configuring resource services

Define the resource service configuration in ResouceServerConfig. The main content of the configuration is to define some matching rules to describe what permissions an access client needs to access a micro service, such as:

@Configuration
public class ResouceServerConfig {
    public static final String RESOURCE_ID = "res1";
    /**
     * Unified authentication service (UAA) resource interception
     */
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends
            ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/uaa/**").permitAll();
        }
    }
    /**
     *  Order service
     */

@Configuration
    @EnableResourceServer
    public class OrderServerConfig extends
        ResourceServerConfigurerAdapter {
            @Autowired
            private TokenStore tokenStore;
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
        }
    }
}

Two microservice resources are defined above, including:

UAAServerConfig specifies that if the request matches / uaa / * * the gateway will not intercept.

OrderServerConfig specifies that if the request matches / order / * *, that is, to access the unified user service, the access client needs to include read in the scope and role in the authorities_ USER.

Because res1 is the access client, read includes ROLE_ADMIN,ROLE_USER,ROLE_API has three permissions.

3.4 security configuration

@Configuration 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .and().csrf().disable();
    }
}

4. Transfer the invention token to the micro service

It is realized through Zuul filter, so that the downstream micro service can easily obtain the current login user information (clear text token)

(1) Implement Zuul prefilter, complete the information extraction of the currently logged in user, and put it into the request of forwarding micro service

 /**
 * token Delivery interception
 */
public class AuthFilter extends ZuulFilter {
    @Override
    public boolean shouldFilter() {
        return true;
    }
    @Override
    public String filterType() {
        return "pre";
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public Object run() {
        /**
         * 1.Get token content
         */
        RequestContext ctx = RequestContext.getCurrentContext();
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!(authentication instanceof OAuth2Authentication)){ // There is no token to access the resources in the gateway. At present, only uua services are directly exposed
            return null;
        }
        OAuth2Authentication oauth2Authentication  = (OAuth2Authentication)authentication;
        Authentication userAuthentication = oauth2Authentication.getUserAuthentication();
        Object principal = userAuthentication.getPrincipal();
        /**
         * 2.Assemble the plaintext token, forward it to the micro service, and put it into the header with the name of JSON token
         */
        List<String> authorities = new ArrayList();
        userAuthentication.getAuthorities().stream().forEach(s ->authorities.add(((GrantedAuthority) s).getAuthority()));
        OAuth2Request oAuth2Request = oauth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication != null){
            jsonToken.put("principal",userAuthentication.getName());
            jsonToken.put("authorities",authorities);
        }
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
        return null;
   }
}

The EncryptUtil class UTF8 is built under the common package to convert to Base64

public class EncryptUtil {
    private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);

    public static String encodeBase64(byte[] bytes){
        String encoded = Base64.getEncoder().encodeToString(bytes);
        return encoded;
    }

    public static byte[]  decodeBase64(String str){
        byte[] bytes = null;
        bytes = Base64.getDecoder().decode(str);
        return bytes;
    }

    public static String encodeUTF8StringBase64(String str){
        String encoded = null;
        try {
            encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            logger.warn("Unsupported encoding format",e);
        }
        return encoded;

    }

    public static String  decodeUTF8StringBase64(String str){
        String decoded = null;
        byte[] bytes = Base64.getDecoder().decode(str);
        try {
            decoded = new String(bytes,"utf-8");
        }catch(UnsupportedEncodingException e){
            logger.warn("Unsupported encoding format",e);
        }
        return decoded;
    }

    public static String encodeURL(String url) {
    	String encoded = null;
		try {
			encoded =  URLEncoder.encode(url, "utf-8");
		} catch (UnsupportedEncodingException e) {
			logger.warn("URLEncode fail", e);
		}
		return encoded;
	}


	public static String decodeURL(String url) {
    	String decoded = null;
		try {
			decoded = URLDecoder.decode(url, "utf-8");
		} catch (UnsupportedEncodingException e) {
			logger.warn("URLDecode fail", e);
		}
		return decoded;
	}

    public static void main(String [] args){
        String str = "abcd{'a':'b'}";
        String encoded = EncryptUtil.encodeUTF8StringBase64(str);
        String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
        System.out.println(str);
        System.out.println(encoded);
        System.out.println(decoded);

        String url = "== wo";
        String urlEncoded = EncryptUtil.encodeURL(url);
        String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
        
        System.out.println(url);
        System.out.println(urlEncoded);
        System.out.println(urlDecoded);
    }


}

(2) To include the filter in the spring container:

Configure AuthFilter

@Configuration
public class ZuulConfig {
    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }
    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

5. Micro service user authentication interception

When the micro service receives a plaintext token, how should it authenticate and intercept it? Implement a filter by yourself? Resolve the plaintext token by yourself and define a set of resource access policies by yourself? Can Spring Security be adapted? Do you suddenly think of the example of Spring Security based on token authentication we implemented earlier. We also take the unified user service as the downstream micro service of the gateway, transform it and increase the user authentication and interception function of the micro service.

(1) Add test resources

OrderController adds the following endpoint s

@PreAuthorize("hasAuthority('p1')")
    @GetMapping(value = "/r1")
    public String r1(){
        UserDTO user = (UserDTO)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
         return user.getUsername() + "Access resource 1";
    }
    @PreAuthorize("hasAuthority('p2')")
    @GetMapping(value = "/r2")
    public String r2(){//Get the current login user through the Spring Security API
        UserDTO user =
(UserDTO)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getUsername() + "Access resource 2";
    }

Add entity class UserDto under model package

@Data
public class UserDTO {
    private String id;
    private String username;
    private String mobile;
    private String fullname;

}

(2) Spring Security configuration

Turn on method protection and add Spring configuration policy. Except that the / login method is not protected (unified authentication needs to be called), all other resources need authentication to access.

@Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
                .and().csrf().disable()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

Based on the above configuration, we have defined three resources. With p1 permission, you can access r1 resources, p2 permission can access r2 resources, and as long as you pass the authentication, you can access r3 resources.

(3) Define the filter to intercept the token and form the Authentication object of Spring Security

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
               String token = httpServletRequest.getHeader("json-token");
        if (token != null){
            //1. Parse token
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            JSONObject userJson = JSON.parseObject(json);
            UserDTO user = new UserDTO();
            user.setUsername(userJson.getString("principal"));
            JSONArray authoritiesArray = userJson.getJSONArray("authorities");
            String  [] authorities = authoritiesArray.toArray( new
String[authoritiesArray.size()]);
            //2. Create and fill in authentication
            UsernamePasswordAuthenticationToken authentication = new
UsernamePasswordAuthenticationToken(
                    user, null, AuthorityUtils.createAuthorityList(authorities));
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                    httpServletRequest));
            //3. Save authentication into security context
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

Through the filter above, the user's identity information can be easily obtained in the resource service:

UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

Or three steps:

1. Parse token

2. Create and fill in authentication

3. Save authentication into security context

Leave the rest to Spring Security.

6 integration test

Note: remember to import eurika coordinates from the pom of uaa and order, as well as application Properties configure eurika

Description of the test process of this case:

1. Oauth2 The password mode of 0 obtains the token from the UAA

2. Use this token to access the test resources of the order service through the gateway

(1) Access the authorization of uaa through the gateway and obtain the token to obtain the token. Note that the port is 53010, the port of the gateway.

If authorized endpoint:

http://localhost:53010/uaa/oauth/authorize?response_type=code&client_id=c1 

Token endpoint

http://localhost:53010/uaa/oauth/token

(2) Use the Token to access the r1-r2 test resources in the order service through the gateway for testing.

result:

Use Zhang San token to access p1, and the access is successful

Using Zhang San token to access p2, the access failed

Li Si token was used to access p1, but the access failed

Use Li Si token to access p2, and the access is successful

Meet the expected results.

(3) Broken token Test

No token test returns:

{ 
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

The returned contents of broken token Test:

{ 
    "error": "invalid_token",
    "error_description": "Cannot convert access token to JSON"
}

7 extended user information

7.1 demand analysis

At present, the jwt token stores the user's identity information and permission information. The gateway forwards the token culture to the micro service for use. At present, the user's identity information only includes the user's account, and the micro service also needs the user's ID, mobile phone number and other important information.

Therefore, this case will provide ideas and methods to expand user information to meet the needs of micro services to use user information.

The scheme of expanding user information in JWT token is analyzed below:

In the authentication phase, DaoAuthenticationProvider will call UserDetailService to query the user's information. Here you can get complete user information. Since the user identity information in the JWT token comes from UserDetails, only username is defined as the user identity information in UserDetails. Here are two ideas: first, UserDetails can be extended to include more custom attributes. Second, the content of username can also be extended, such as storing json data content as the content of username. In comparison, scheme 2 is relatively simple without destroying the structure of UserDetails. We adopt scheme 2.

7.2 modifying UserDetailService

Query the user from the database, convert the overall user into json and store it in the userDetails object.

@Override 
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //Login account
    System.out.println("username="+username);
    //Query the database according to the account number
    UserDto user = userDao.getUserByUsername(username);
    if(user == null){
        return null;
    }
    //Query user permissions
    List<String> permissions = userDao.findPermissionsByUserId(user.getId());
    String[] perarray = new String[permissions.size()];
    permissions.toArray(perarray);
    //Create userDetails
    //Here, the user is converted to json, and the overall user is stored in userDetails
    String principal = JSON.toJSONString(user);
    UserDetails userDetails =
User.withUsername(principal).password(user.getPassword()).authorities(perarray).build();
    return userDetails;
}

7.3 modify resource service filter

The filter in the resource service is responsible for parsing the JSON token from the header, from which you can get the user identity information put in by the gateway. Some key codes are as follows:

... 
if (token != null){
    //1. Parse token
    String json = EncryptUtil.decodeUTF8StringBase64(token);
    JSONObject userJson = JSON.parseObject(json);
    //Retrieve user identity information
    String principal = userJson.getString("principal");
    //Convert json to object
    UserDTO userDTO = JSON.parseObject(principal, UserDTO.class);
    JSONArray authoritiesArray = userJson.getJSONArray("authorities");
    ...

The above process completes the scheme of customizing user identity information.

Keywords: Spring Spring Cloud Spring Security eureka

Added by daedalus__ on Wed, 09 Feb 2022 06:08:18 +0200