The commonly used Security frameworks under ava mainly include Spring Security and shiro, which can provide very powerful functions, but the learning cost is high. Under microservice, authentication will be more or less invasive to the service. In order to reduce dependence, reduce intrusion and make the authentication function transparent to the application service, we use the gateway to intercept resource requests for authentication.
1, Overall architecture
The user authentication module is located in the API GateWay service, and all API resource requests need to pass from here.
do If the authentication is passed, the user authority data will be cached. If not, the user authority data will be returned 401do For user authentication, compare whether the current access resources (URI and Method) are in the cached user permission data. If they are, they will forward the request to the corresponding application service, and if they are not, they will return 403
2, Implementation steps
1. User login
public LoginUser login(String userName, String password){ // Check password User user = userService.checkUser(userName, password); LoginUser loginUser = LoginUser.builder() .userName(userName) .realName(user.getRealName()) .userToken(UUID.randomUUID().toString()) .loginTime(new Date()) .build(); // Save session session.saveSession(loginUser); // Query authority List<Permission> permissions = permissionRepository.findByUserName(userName); // Save user permissions to cache session.saveUserPermissions(userName, permissions); return loginUser; } // ... // Cache user permissions to Redis public void saveUserPermissions(String userName, List<Permission> permissions) { String key = String.format("login:permission:%s", userName); HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash(); hashOperations.putAll(key, permissions.stream().collect( Collectors.toMap(p -> p.getMethod().concat(":").concat(p.getUri()), Permission::getName, (k1, k2) -> k2))); if (expireTime != null) { redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); } }
- After the user passes the authentication, issue the userToken to save the current login information and cache the user authorization list
- When caching the authorization list, in order to facilitate reading, use the hash method to save it as a list. Do not directly save the array object as an object
2. Intercept request
@Slf4j @Component public class AuthorizationFilter extends AbstractGatewayFilterFactory { @Autowired private Session session; @Override public GatewayFilter apply(Object config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String uri = request.getURI().getPath(); String method = request.getMethodValue(); // 1. Get userName from AuthenticationFilter String key = "X-User-Name"; if (!request.getHeaders().containsKey(key)) { response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); } String userName = Objects.requireNonNull(request.getHeaders().get(key)).get(0); // 2. Verify permissions if (!session.checkPermissions(userName, uri, method)) { log.info("User:{}, No permission", userName); response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); } return chain.filter(exchange); }; } }
- Step 1: take out the X-User-Name passed by the identity authentication module
- The second step is to check whether there are corresponding permissions in the cache
public boolean checkPermissions(String userName, String uri, String method) { String key = String.format("login:permission:%s", userName); String hashKey = String.format("%s:%s", method, uri); if (redisTemplate.opsForHash().hasKey(key, hashKey)){ return true; } String allKey = "login:permission:all"; // If not in the permission list, pass return !redisTemplate.opsForHash().hasKey(allKey, hashKey); }
- If not in the permission list, pass It is mainly to let go of some unnecessary public resources, which can be accessed by default
- login:permission:all All configured permission lists need to be put into the cache when the program starts, and the data needs to be kept updated
3. Authentication Filter configuration
spring: cloud: gateway: routes: - id: cloud-user uri: lb://Cloud user # backend service name predicates: - Path=/user/** # Routing address filters: - name: AuthenticationFilter # identity authentication - name: AuthorizationFilter # User authentication - StripPrefix=1 # Remove prefix
- Pay special attention to the order of filter s. Identity authentication must be done before authentication
- If more routes need to be configured, you can use the default filters default Filter configuration
3, Other issues
During unit testing, if the following errors are encountered
nested exception is java.lang.NoClassDefFoundError: javax/validation/ValidationException
Please upgrade the dependent package version:
<!--upgrade validation-api Version of--> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.5.Final</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>