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.