This is the 7th day of my participation in Gwen Challenge. This article is participating in “Java Theme Month – Java Development in Action”, see the activity link for details.

1. Review the design concept of microservices

In shallow into Spring Cloud architecture, we know that what is micro service, micro services division, in fact, after all, the design of the micro service, has its unique advantages: make the decoupling between the various modules, each module has its own independent soul, other service even if no problem, he will not affected by any. This is the core tenet of microservices. So today we are going to talk about the security of micro services, in fact, also reflects a core of micro services: high cohesion. So-called high cohesion, simple to understand is that external exposure of minimal, reduce its dependencies, mostly as a black box packaging, not directly abroad, in this way, even if the internal changes, speculating, external interface doesn’t change, that is good service design idea, perfect foreign compatible, a good architectural design, first of all, This point may need to get in place, I do not know what you think? So today’s microservice security has a little bit to do with this high cohesion. In other words, it embodies the core concept of micro-service design.

2. Various security guarantees under micro-services

2.1 Common security measures

In microservices, we commonly take the following security design measures: gateway design, limit of service port exposure, token authentication, unified authentication of OAuth2, openId design in wechat, etc. These are some of the measures taken to consider the security of the service.

2.2 Concept of OAuth2

What is OAuth2? Let’s first learn about OAuth. OAuth is an open standard. Suppose there is a scenario: a QQ application hopes to let a third-party (MOOC) application get some information about itself (unique user id, such as QQ number, user personal information, some basic information, nickname and profile picture, etc.). However, while obtaining this information, they cannot provide information such as user names and passwords.

OAuth is a specification to achieve the above goals. OAuth2 is a continuation of the OAuth protocol, but not compatible with OAuth1.0, that is, completely deprecated OAuth1.0.

OAuth2.0 has a few terms: customer credentials, tokens, scopes.

  • Customer credentials: The customer’s clientId and password are used to authenticate the customer.

  • Token: A token issued by the authorization server upon receipt of a customer request.

  • Scope: The additional subdivision of authority specified by the resource owner when a client requests access to a token.

2.3 Principle of OAuth2

There are four core objects in OAuth2’s authorization mechanism:

Resource Owner: Indicates the Resource Owner.

Client: third-party access platform, application, and requestor.

Resource Server: a Resource Server that stores user information and user Resource information.

Authorization Server: indicates the Authorization Server.

Implementation mechanism:

  • When a user clicks on the third-party application to log in, the application sends a request to the authentication server, stating that a user wants to perform authorization operations, and at the same time explaining who he or she is and the callback URL after the user has completed authorization. For example, as shown in the screenshot above, the user accesses QQ through MOOC to obtain authorization.

  • The authentication server displays its own authorization interface to users.

  • After the user performs an authorization operation, the authentication server generates an authorization code and redirects to the callback URL of the third party.

  • After the third-party application gets the code, it sends it to the authentication server together with its own identity information (ID and password) on the platform to verify the request again, indicating that its identity is correct and the user has authorized me to exchange for access to user resources.

  • The authentication server verifies the request information, and if it is ok, generates the access_token to access the resource server and hands it to the third-party application.

  • A third-party application uses an Access_token to request resources from the resource server.

  • The resource server validates the Access_token successfully and returns the response resource.

2.4 Several authorization modes of OAuth2

OAuth2.0 has several authorization modes: authorization code mode, simplified mode, password mode, client credentials mode.

Authorization code mode :(authorization_code) is the authorization mode with the most complete functions and the most rigorous process. Code ensures the security of tokens. Even if code is intercepted, tokens cannot be obtained through code because there is no client_secret.

Simplified mode: Similar to the authorization code mode, but with fewer steps to obtain the code, the token is directly obtained. It is suitable for public browser single-page applications. The token is returned directly from the authorization server, and does not support refreshing the token.

Password mode: The user name and password are used as the authorization mode to obtain the token from the authorization server. Token refreshing is not supported.

Client-side credential mode: Commonly used in the resource server is a back-end module of the application where the client authenticates its identity to the authentication server to obtain the token.

2.5 Actual OAuth2 password mode

Combined with Spring Cloud Alibaba component, this paper mainly explains the part of OAuth2 to realize the micro service security system.

First, take a look at the authentication center. The authentication center needs to provide single point of service to escort the security of all client micro services. Let’s look at dependencies first:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Copy the code

If you need to use Redis to store tokens, you can add reIDS dependencies. If you use JWT, you can use:

<dependency> <groupId> IO. Jsonwebtoken </groupId> <artifactId> JJWT </artifactId> <version>0.9.0</version> </dependency>Copy the code

Of course, this project module introduces a relatively new Spring Boot:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> The < version > 2.1.13. RELEASE < / version > < relativePath / > < / parent >Copy the code

The rest, like databases, persistence, etc., can be added as needed.

After the configuration is complete, we need to write a configuration for the authentication server:

* / / * * * redis token way @ Override public void the configure (AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .exceptionTranslator(userOAuth2WebResponseExceptionTranslator) .userDetailsService(loginService) .tokenStore(tokenStore); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("provider-service") .secret(passwordEncoder.encode("provider-service-123")) .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(864000) .autoApprove(true) .scopes("all") .authorizedGrantTypes("password", "authorization_code", "client_credentials", "refresh_token") .redirectUris("http://localhost:2001/login") } @Override public void configure(AuthorizationServerSecurityConfigurer security) { security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()"); }}Copy the code

Redis configuration:

package com.damon.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; @Configuration public class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ //return new RedisTokenStore(redisConnectionFactory); return new MyRedisTokenStore(redisConnectionFactory); }}Copy the code

SpringSecurity is required to configure interception of secure access:

@Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()

                .exceptionHandling()
        		.authenticationEntryPoint(new AuthenticationEntryPointHandle())
        		//.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
        		.and()

                .authorizeRequests()
                .antMatchers("/oauth/**", "/login/**")//"/logout/**"
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll();
    }
Copy the code

Furthermore, resource interception needs to be configured:

package com.damon.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointHandle()) //.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers("/api/**") .and() .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .httpBasic(); }}Copy the code

Above, we have configured the unified processing configuration of resource interception and permission interception:

public class AuthenticationEntryPointHandle implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { //response.setStatus(HttpServletResponse.SC_FORBIDDEN); //response.setStatus(HttpStatus.OK.value()); //response.setHeader("Access-Control-Allow-Origin", "*"); Response. setHeader(" access-Control-allow-headers ", "token"); response.setHeader(" access-Control-allow-headers ", "token"); response.setHeader(" access-Control-allow-headers ", "token"); Responsetheader (" x-frame-options ", "SAMEORIGIN"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.getWriter() .write(JSON.toJSONString(Response.ok(response.getStatus(), -2, authException.getMessage(), null))); /*response.getWriter() .write(JSON.toJSONString(Response.ok(200, -2, "Internal Server Error", authException.getMessage()))); * /}}Copy the code

Finally, we may need to configure some requesting client configuration, as well as variable configuration:

@Configuration public class BeansConfig { @Resource private Environment env; @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(env.getProperty("client.http.request.readTimeout", Integer.class, 15000)); requestFactory.setConnectTimeout(env.getProperty("client.http.request.connectTimeout", Integer.class, 3000)); RestTemplate rt = new RestTemplate(requestFactory); return rt; }}Copy the code

Finally, some environment configurations need to be configured:

Spring: Application: name: oauth-cas cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 refreshable - dataids: physical properties, the properties redis: # redis related configuration database: 8 host: Jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000msCopy the code

Remember: the bootstrap configuration above needs to be added in the bootstrap file, otherwise it may fail, so try it.

server: port: 2000 undertow: uri-encoding: UTF-8 accesslog: enabled: false pattern: Servlet: session: timeout: PT120M Cookie: name: oauth-cas-sessionIDCopy the code

Finally, we add the startup class:

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
public class CasApp {
	public static void main(String[] args) {
		SpringApplication.run(CasApp.class, args);
	}
}
Copy the code

Above, a certification center code actual combat logic is completed.

Next, let’s look at how a client can authenticate, again relying on:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</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-netflix-hystrix</artifactId>
</dependency>
Copy the code

On the client side, we also need to configure a resource configuration and permission configuration:

@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointHandle()) //.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers("/api/**") .and() .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .httpBasic(); }}Copy the code

Of course, permission interception might be relatively simple:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(101)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
Copy the code

Again, we need a unified result handling class, which we won’t show here.

Next, let’s focus on configuration:

Cas-server-url: http://oauth-cas #http://localhost:2000# Set the address that can be accessed security: oauth2: # Configure the configuration corresponding to the CAS client: client-id: provider-service client-secret: provider-service-123 user-authorization-uri: ${cas-server-URL}/oauth/authorize # specifies the access-tok-URI required for authorization code authentication. Resource: loadBalanced: true # JWT: # JWT: # JWT: ${cas-server-url}/oauth/token # ${cas-server-url}/oauth/token_key #key-value: test_jwt_sign_key ID: provider-service # ${cas-server-url}/ API /user # specifies the user info URI. The original address suffix is /auth/user prefer-token-info: false #token-info-uri: authorization: check-token-access: ${cas-server-url}/oauth/check_token # When the Web server receives the request from the UI client, it needs to take the token in the request to the authentication server for token verification, which is the requested interfaceCopy the code

In the configuration above, we see various comments, explained in detail, but I want to emphasize: for high availability, we may have more than one certification authority, so we need the domain name for LB. LoadBalanced =true is also enabled. Finally, in the authorization code authentication mode, “user-authorization-URI” is required; in the password mode, “access-token-uri” is required to obtain the token. We use it “user-info-uri” to obtain the user information of the authentication center, so as to determine the permission of the user, so as to access corresponding resources. In addition, the above configuration must be in the bootstrap file, otherwise it may fail, you can try.

Next, we add the general configuration:

server: port: 2001 undertow: uri-encoding: UTF-8 accesslog: enabled: false pattern: combined servlet: session: timeout: PT120M cookie: name: provider-service-sessionId # Prevent cookie conflicts, which may cause login authentication failure backend: ribbon: client: enabled: True ServerListRefreshInterval: 5000 ribbon: ConnectTimeout: 3000 # set the global default ribbon read timeout ReadTimeout: 1000 eager - load: Enabled: true clients: request - cas, consumer - service MaxAutoRetries: 1 # service for the first request of retries MaxAutoRetriesNextServer: 1 # to retry the next service the maximum number of (not including the first service) # listOfServers: localhost: 5556, localhost: 5557 # ServerListRefreshInterval: 2000 OkToRetryOnAllOperations: true NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000 hystrix.threadpool.BackendCallThread.coreSize: 5Copy the code

Here, we use the Ribbon to do LB and Hystrix to do fuses. Finally, we need to note that cookie name is added to prevent cookie conflicts, which may lead to login authentication failure.

Configure the startup class:

@Configuration @EnableAutoConfiguration @ComponentScan(basePackages = {"com.damon"}) @EnableDiscoveryClient @EnableOAuth2Sso public class ProviderApp { public static void main(String[] args) { SpringApplication.run(ProviderApp.class, args); }}Copy the code

We have configured that all path requests with “/ API /**” will be intercepted to determine whether the user has access permission based on their information.

Write a simple test class:

@RestController @RequestMapping("/api/user") public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @Autowired private UserService userService; @PreAuthorize("hasAuthority('admin')") @GetMapping("/auth/admin") public Object adminAuth() { logger.info("test password  mode"); return "Has admin auth!" ; }}Copy the code

The code above says: if the user has the “admin” permission, they can access the interface, otherwise they will be rejected.

This paper uses alibaba components to do LB. For details, you can see the previous article and use domain name to find services. Gateway is also added.

Finally, let’s first use password mode for authentication:

curl -i -X POST -d "username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123" http://localhost:5555/oauth-cas/oauth/token
Copy the code

After the authentication is successful, the following information is displayed:

{"access_token":"d2066f68-665b-4038-9dbe-5dd1035e75a0","token_type":"bearer","refresh_token":"44009836-731c-4e6a-9cc3-27 4ce3af8c6b","expires_in":3599,"scope":"all"}Copy the code

Next, we use tokens to access the interface:

curl -i -H "Accept: application/json" -H "Authorization:bearer d2066f68-665b-4038-9dbe-5dd1035e75a0" -X GET http://localhost:5555/provider-service/api/user/auth/admin
Copy the code

Success returns the result:

Has admin auth!
Copy the code

If token is invalid, return:

{"error":"invalid_token","error_description":"d2066f68-665b-4038-9dbe-5dd1035e75a01"}
Copy the code








3. GitHub authorization application case

If your app wants to access GitHub, here’s how to do it.

  1. First sign up for a GitHub account, log in, go to the Settings, open the page, there is a developer Settings at the bottom:

After finding it, click, you can see three, you can choose the second way to access:

You can add your app app. When creating a new app, the app name and callback address must be filled in:

Finally, a Client ID, Client Secret, is generated after completion.

Then use Github official documents for authentication, access and authorization logic:

1. Client ID and Client Secret are generated after the information is registered. First, the user clicks Github to log in to the local application and directs the user to the third-party authorization page.

https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&state={state}
Copy the code

Client_id and client_secret are provided by Github after Oauth APP is registered and need to be written in local code or configuration file. State is also generated locally. Redirect_uri is the Authorization Callback URL specified on GitHub’s official website. At this time, we apply for authorization with parameters such as state, but we have not logged in yet, and failed to return code parameters through authorize and GitHub.

2. After authorization is successful, the github address will be redirected with a parameter to access the above redirect_URI and a code parameter will be added to the background to receive the code parameter.

https://github.com/login/oauth/access_token?client_id=xxx&client_secret=xxx&code=xxx&redirect_uri=http://localhost:3001/ authCallbackCopy the code

Note: The redirect_URI specified above must be the same as that specified when creating the app, otherwise an error will be reported.

3. Obtain the access_token using the state and code parameters. After obtaining the access_token, you only need to put the access_token after the URL to exchange user information. Visit address:

https://api.github.com/user?access_token=xxx
Copy the code

4. If you have obtained the personal information of the GitHub authorized user, the authorization is successful.

4. Design of micro service security architecture

Security is an important issue in microservices. The most common scenario is that service A needs to call service B, but the question arises: should we call it from the Internet? Or is it a LAN call? Of course, it depends on whether A and B are in the same network segment. If they are in the same LAN segment, they must go LAN. Why is that? Because LAN is fast, if there is a reason? Of course: in addition to fast network, reduce network overhead, but also to ensure security, not to be hacked. This is a guarantee of safety.

So besides the security mentioned above, what else? For example, in a LAN, there are N microservice modules, but these microservices do not want to be directly exposed to the outside world. In this case, a Gateway is required. The gateway gives all services to the routing, like a layer on top of all services, adding a protective halo to highlight the meaning of high cohesion. Also can add some interception, security interception, authentication, authentication and so on. There is authentication by token, there can be authentication by JWT, and so on. Sometimes you can use Redis to share via session. Security interception can also be achieved through OAuth2’s authentication mode.

The final security consideration is in the interface design of each service, such as the existence of idempotent, which makes many malicious attacks useless.