Previous reviews and integrated design
Design and implementation of authentication and API authority control in microservice architecture
Microservice Gateway
Microservice architecture permission
- The user is not logged in. The client (Web and mobile) initiates the login request, and the gateway directly forwards the login request to the Auth service. The Auth service verifies the user’s identity information (the integration project omits the user system, and the readers can implement it by themselves, directly returning the user information by hard coding), and finally returns the legitimate token to the client.
- The user has logged in to request another service. In this case, when the client request reaches the gateway, the gateway will call the Auth system to verify the legitimacy of the request. If the authentication fails, it will directly reject the request and return 401. If the user passes the authentication, it is forwarded to a specific service. The service passes the filter and obtains the security permission information of the user based on the userId in the request header. Proceed validates the permissions required by the interface using the aspect; otherwise 403 is returned.
Gateway implementation
Microservice Gateway
- Distinguish between exposed interfaces (that is, direct external access) and interfaces that require legitimate login
- The exposed interface is directly allowed and forwarded to specific services, such as login and token refresh
- The interface that needs to be accessed after logging in with a legal identity, according to the incoming Access token to construct the head, the head mainly includes userId and other information, can be set in the AuTH service according to their actual business.
- Last but not least, introduce the resource server configuration of Spring Security. For the exposed interface set permitAll(), the other interfaces enter the process of identity verification, call the Auth service, if passed, continue to forward normally, otherwise throw an exception, return 401.
Gateway Routing Flowchart
PermitAll implementation
auth:
permitall:
-
pattern: /login/**
-
pattern: /web/public/**
Copy the code
@Bean
@ConfigurationProperties(prefix = "auth")
public PermitAllUrlProperties getPermitAllUrlProperties() {
return new PermitAllUrlProperties();
}
Copy the code
To strengthen the head
public class HeaderEnhanceFilter implements Filter { //... @Autowired private PermitAllUrlProperties permitAllUrlProperties; @override public void init(FilterConfig FilterConfig) throws ServletException {} // The main filtering method @override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { String authorization = ((HttpServletRequest) servletRequest).getHeader("Authorization"); String requestURI = ((HttpServletRequest) servletRequest).getRequestURI(); // test if request url is permit all , then remove authorization from header LOGGER.info(String.format("Enhance request URI : %s.", requestURI)); If (isPermitAllUrl(requestURI) &&isNoToAuthEndpoint (requestURI)) {// Remove headers, But does not include the login endpoint head it resetRequest = removeValueFromRequestHeader (servletRequest) (it); filterChain.doFilter(resetRequest, servletResponse); return; } // Check whether the header meets the specification if (stringutils. isNotEmpty(authorization)) {if (isJwtBearerToken(authorization)) {try {authorization = StringUtils.substringBetween(authorization, "."); String decoded = new String(Base64.decodeBase64(authorization)); Map properties = new ObjectMapper().readValue(decoded, Map.class); // Resolve tokens in authorization. Construct USER_ID_IN_HEADER String userId = (String) properties.get(SecurityConstants.USER_ID_IN_HEADER); RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, userId); } catch (Exception e) { LOGGER.error("Failed to customize header for the request", e); }}} else {// To fit, Set the anonymous head RequestContext. GetCurrentContext (.) addZuulRequestHeader (SecurityConstants. USER_ID_IN_HEADER ANONYMOUS_USER_ID); } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } //... }Copy the code
Resource Server Configuration
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //... // Set request pattern for permitAll. Override public void configure(HttpSecurity HTTP) throws Exception {http.csrf().disable() .requestMatchers().antMatchers("/**") .and() .authorizeRequests() .antMatchers(permitAllUrlProperties.getPermitallPatterns()).permitAll() .anyRequest().authenticated(); } / / through custom CustomRemoteTokenServices, Implanted identity legitimacy related validation @ Override public void the configure (ResourceServerSecurityConfigurer resources) throws the Exception { CustomRemoteTokenServices resourceServerTokenServices = new CustomRemoteTokenServices(); / /... resources.tokenServices(resourceServerTokenServices); }}Copy the code
<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>
Copy the code
Custom implementation of RemoteTokenServices
Queries the /check_token endpoint to obtain the contents of an access token.
If the endpoint returns a 400 response, this indicates that the token is invalid.
security:
oauth2:
client:
accessTokenUri: /oauth/token
clientId: gateway
clientSecret: gateway
resource:
userInfoUri: /user
token-info-uri: /oauth/check_token
Copy the code
emphasize
Auth integration
@requestMapping (method = requestmethod. GET, value = "/ API /userPermissions? userId={userId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) List<Permission> getUserPermissions(@RequestParam("userId") String userId); AccessLevel = accessLevel = accessLevel = accessLevel = accessLevel = accessLevel = accessLevel = accessLevel @RequestMapping(method = RequestMethod.GET, value = "/api/userAccesses? userId={userId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) List<UserAccess> getUserAccessList(@RequestParam("userId") String userId);Copy the code
Backend project implementation
- The header of the gateway construct, userId (and possibly other information, for example here), can be obtained in Backend
- Requests forwarded to the Backend service are authenticated or are directly exposed interfaces
- The Auth service provides an interface for obtaining permissions based on the userId
The flow chart of backend
The Filter Filter
public class AuthorizationFilter implements Filter { @Autowired private FeignAuthClient feignAuthClient; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain FilterChain) throws IOException, ServletException {logger.info(" Filter executing..." ); // pass the request along the filter chain String userId = ((HttpServletRequest) servletRequest).getHeader(SecurityConstants.USER_ID_IN_HEADER); if (StringUtils.isNotEmpty(userId)) { UserContext userContext = new UserContext(UUID.fromString(userId)); userContext.setAccessType(AccessType.ACCESS_TYPE_NORMAL); List<Permission> permissionList = feignAuthClient.getUserPermissions(userId); List<SimpleGrantedAuthority> authorityList = new ArrayList<>(); for (Permission permission : permissionList) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(); authority.setAuthority(permission.getPermission()); authorityList.add(authority); } CustomAuthentication userAuth = new CustomAuthentication(); userAuth.setAuthorities(authorityList); userContext.setAuthorities(authorityList); userContext.setAuthentication(userAuth); SecurityContextHolder.setContext(userContext); } filterChain.doFilter(servletRequest, servletResponse); } / /... }Copy the code
Implement @preauth annotations through facets
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
String value();
}
Copy the code
@Component @Aspect public class AuthAspect { @Pointcut("@annotation(com.blueskykong.auth.demo.annotation.PreAuth)") Private void cut() {} private void cut() {** * * * @param joinPoint * @param preAuth */ @around ("cut()&&@annotation(preAuth)") public Object Parameter Description Parameter Description Value (joinPoint joinPoint, PreAuth PreAuth) throws Throwable {// Take the expression String value = preauth.value (); / / Spring EL to parse the value SecurityExpressionOperations operations = new CustomerSecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()); StandardEvaluationContext operationContext = new StandardEvaluationContext(operations); ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(value); Boolean result = expression.getValue(operationContext, boolea.class); If (result) {// Proceed with the method in the interface return joinPoint.proceed(); } return "Forbidden"; }}Copy the code
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot { public CustomerSecurityExpressionRoot(Authentication authentication) { super(authentication); }}Copy the code
The Controller interface
@requestMapping (value = "/test", method = requestmethod.get) @preauth ("hasAuthority('CREATE_COMPANY')") HasRole ('Admin') public String test() {return "ok"; }Copy the code
Why is it designed this way?
The late optimization
- The current design is that each request will call the Auth service to obtain the corresponding permission information of the user. There are a lot of back-end microservices, so it is not necessary for each service, or multiple service instances of a service, to call Auth service every time. The author thinks that it is completely possible to introduce the caching mechanism of Redis cluster, and when the request reaches an instance of a service, the user’s permissions in the cache should be queried first. If the Auth service is not called again, the Redis cache is written last. Of course, if the permissions are updated, the Auth service must delete the corresponding user permissions cache.
- As for the rejected request, the object is directly returned in the section expression. The author thinks that it can be bound with Response Status 403 to customize the content of the returned object, and the response returned is more friendly.
conclusion
The source code
- GitHub:github.com/keets2012/A…
- Yards cloud: gitee.com/keets/Auth-…
- GitHub:github.com/keets2012/S…
- Yards cloud: gitee.com/keets/sprin…
Blueskykong.com/2017/12/10/…