[TOC]java
在微服务的用户认证与受权杂谈(上)一文中简单介绍了微服务下常见的几种认证受权方案,而且使用JWT编写了一个极简demo来模拟Token的颁发及校验。而本文的目的主要是延续上文来补充几个要点,例如Token如何在多个微服务间进行传递,以及如何利用AOP实现登陆态和权限的统一校验。node
为了让登陆态的检查逻辑可以通用,咱们通常会选择使用过滤器、拦截器以及AOP等手段来实现这个功能。而本小节主要是介绍使用AOP实现登陆状态检查,由于利用AOP一样能够拦截受保护的资源访问请求,在对资源访问前先作一些必要的检查。web
首先须要在项目中添加AOP的依赖:面试
<!-- AOP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定义一个注解,用于标识哪些方法在被访问以前须要进行登陆态的检查。具体代码以下:spring
package com.zj.node.usercenter.auth; /** * 被该注解标记的方法都须要检查登陆状态 * * @author 01 * @date 2019-09-08 **/ public @interface CheckLogin { }
编写一个切面,实现登陆态检查的具体逻辑,代码以下:apache
package com.zj.node.usercenter.auth; import com.zj.node.usercenter.util.JwtOperator; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * 登陆态检查切面类 * * @author 01 * @date 2019-09-08 **/ @Slf4j @Aspect @Component @RequiredArgsConstructor public class CheckLoginAspect { private static final String TOKEN_NAME = "X-Token"; private final JwtOperator jwtOperator; /** * 在执行@CheckLogin注解标识的方法以前都会先执行此方法 */ @Around("@annotation(com.zj.node.usercenter.auth.CheckLogin)") public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable { // 获取request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 从header中获取Token String token = request.getHeader(TOKEN_NAME); // 校验Token是否合法 Boolean isValid = jwtOperator.validateToken(token); if (BooleanUtils.isFalse(isValid)) { log.warn("登陆态校验不经过,无效的Token:{}", token); // 抛出自定义异常 throw new SecurityException("Token不合法!"); } // 校验经过,能够设置用户信息到request里 Claims claims = jwtOperator.getClaimsFromToken(token); log.info("登陆态校验经过,用户名:{}", claims.get("userName")); request.setAttribute("id", claims.get("id")); request.setAttribute("userName", claims.get("userName")); request.setAttribute("role", claims.get("role")); return joinPoint.proceed(); } }
而后编写两个接口用于模拟受保护的资源和获取token。代码以下:json
@Slf4j @RestController @RequestMapping("/users") @RequiredArgsConstructor public class UserController { private final UserService userService; private final JwtOperator jwtOperator; /** * 须要校验登陆态后才能访问的资源 */ @CheckLogin @GetMapping("/{id}") public User findById(@PathVariable Integer id) { log.info("get request. id is {}", id); return userService.findById(id); } /** * 模拟生成token * * @return token */ @GetMapping("gen-token") public String genToken() { Map<String, Object> userInfo = new HashMap<>(); userInfo.put("id", 1); userInfo.put("userName", "小眀"); userInfo.put("role", "user"); return jwtOperator.generateToken(userInfo); } }
最后咱们来进行一个简单的测试,看看访问受保护的资源时是否会先执行切面方法来检查登陆态。首先启动项目获取token:架构
在访问受保护的资源时在header中带上token:app
访问成功,此时控制台输出以下:ide
Tips:
这里之因此没有使用过滤器或拦截器来实现登陆态的校验,而是采用了AOP,这是由于使用AOP写出来的代码比较干净而且能够利用自定义注解实现可插拔的效果,例如访问某个资源不用进行登陆态检查了,那么只须要把
@CheckLogin
注解给去掉便可。另外就是AOP属于比较重要的基础知识,也是在面试中常常被问到的知识点,经过这个实际的应用例子,可让咱们对AOP的使用技巧有必定的了解。固然也能够选择过滤器或拦截器来实现,没有说哪一种方式就是最好的,毕竟这三种方式都有各自的特性和优缺点,须要根据具体的业务场景来选择。
在微服务架构中一般会使用Feign来调用其余微服务所提供的接口,若该接口须要对登陆态进行检查的话,那么就得传递当前客户端请求所携带的Token。而默认状况下Feign在请求其余服务的接口时,是不会携带任何额外信息的,因此此时咱们就得考虑如何在微服务之间传递Token。
让Feign实现Token的传递仍是比较简单的,主要有两种方式,第一种是使用Spring MVC的@RequestHeader
注解。以下示例:
@FeignClient(name = "order-center") public interface OrderCenterService { @GetMapping("/orders/{id}") OrderDTO findById(@PathVariable Integer id, @RequestHeader("X-Token") String token); }
Controller里的方法也须要使用这个注解来从header中获取Token,而后传递给Feign。以下:
@RestController @RequiredArgsConstructor public class TestController { private final OrderCenterService orderCenterService; @GetMapping("/{id}") public OrderDTO findById(@PathVariable("id") Integer id, @RequestHeader("X-Token") String token) { return orderCenterService.findById(id, token); } }
从上面这个例子能够看出,使用@RequestHeader
注解的优势就是简单直观,而缺点也很明显。当只有一两个接口须要传递Token时,这种方式仍是可行的,但若是有不少个远程接口须要传递Token的话,那么每一个方法都得加上这个注解,显然会增长不少重复的工做。
因此第二种传递Token的方式更为通用,这种方式是经过实现一个Feign的请求拦截器,而后在拦截器中获取当前客户端请求所携带的Token并添加到Feign的请求header中,以此实现Token的传递。以下示例:
package com.zj.node.contentcenter.feignclient.interceptor; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * 请求拦截器,实如今服务间传递Token * * @author 01 * @date 2019-09-08 **/ public class TokenRelayRequestInterceptor implements RequestInterceptor { private static final String TOKEN_NAME = "X-Token"; @Override public void apply(RequestTemplate requestTemplate) { // 获取当前的request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 从header中获取Token String token = request.getHeader(TOKEN_NAME); // 传递token requestTemplate.header(TOKEN_NAME,token); } }
而后须要在配置文件中,配置该请求拦截器的包名路径,否则不会生效。以下:
# 定义feign相关配置 feign: client: config: # default即表示为全局配置 default: requestInterceptor: - com.zj.node.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor
除了Feign之外,部分状况下有可能会使用RestTemplate来请求其余服务的接口,因此本小节也介绍一下,在使用RestTemplate的状况下如何实现Token的传递。
RestTemplate也有两种方式能够实现Token的传递,第一种方式是请求时使用exchange()
方法,由于该方法能够接收header。以下示例:
@RestController @RequiredArgsConstructor public class TestController { private final RestTemplate restTemplate; @GetMapping("/{id}") public OrderDTO findById(@PathVariable("id") Integer id, @RequestHeader("X-Token") String token) { // 传递token HttpHeaders headers = new HttpHeaders(); headers.add("X-Token", token); return restTemplate.exchange( "http://order-center/orders/{id}", HttpMethod.GET, new HttpEntity<>(headers), OrderDTO.class, id).getBody(); } }
另外一种则是实现ClientHttpRequestInterceptor
接口,该接口是RestTemplate的拦截器接口,与Feign的拦截器相似,都是用来实现通用逻辑的。具体代码以下:
public class TokenRelayRequestInterceptor implements ClientHttpRequestInterceptor { private static final String TOKEN_NAME = "X-Token"; @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 获取当前的request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest servletRequest = attributes.getRequest(); // 从header中获取Token String token = servletRequest.getHeader(TOKEN_NAME); // 传递Token request.getHeaders().add(TOKEN_NAME,token); return execution.execute(request, body); } }
最后须要将实现的拦截器注册到RestTemplate中让其生效,代码以下:
@Configuration public class BeanConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList( new TokenRelayRequestInterceptor() )); return restTemplate; } }
在第一小节中咱们介绍了如何使用AOP实现登陆态检查,除此以外某些受保护的资源可能须要用户拥有特定的权限才可以访问,那么咱们就得在该资源被访问以前作权限校验。权限校验功能一样也可使用过滤器、拦截器或AOP来实现,和以前同样本小节采用AOP做为示例。
这里也不作太复杂的校验逻辑,主要是判断用户是不是某个角色便可。因此首先定义一个注解,该注解有一个value,用于标识受保护的资源须要用户为哪一个角色才容许访问。代码以下:
package com.zj.node.usercenter.auth; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 被该注解标记的方法都须要检查用户权限 * * @author 01 * @date 2019-09-08 **/ @Retention(RetentionPolicy.RUNTIME) public @interface CheckAuthorization { /** * 容许访问的角色名称 */ String value(); }
而后定义一个切面,用于实现具体的权限校验逻辑。代码以下:
package com.zj.node.usercenter.auth; import com.zj.node.usercenter.util.JwtOperator; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * 权限验证切面类 * * @author 01 * @date 2019-09-08 **/ @Slf4j @Aspect @Component @RequiredArgsConstructor public class AuthAspect { private static final String TOKEN_NAME = "X-Token"; private final JwtOperator jwtOperator; /** * 在执行@CheckAuthorization注解标识的方法以前都会先执行此方法 */ @Around("@annotation(com.zj.node.usercenter.auth.CheckAuthorization)") public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable { // 获取request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 从header中获取Token String token = request.getHeader(TOKEN_NAME); // 校验Token是否合法 Boolean isValid = jwtOperator.validateToken(token); if (BooleanUtils.isFalse(isValid)) { log.warn("登陆态校验不经过,无效的Token:{}", token); // 抛出自定义异常 throw new SecurityException("Token不合法!"); } Claims claims = jwtOperator.getClaimsFromToken(token); String role = (String) claims.get("role"); log.info("登陆态校验经过,用户名:{}", claims.get("userName")); // 验证用户角色名称是否与受保护资源所定义的角色名称匹配 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); CheckAuthorization annotation = signature.getMethod() .getAnnotation(CheckAuthorization.class); if (!annotation.value().equals(role)) { log.warn("权限校验不经过!当前用户角色:{} 容许访问的用户角色:{}", role, annotation.value()); // 抛出自定义异常 throw new SecurityException("权限校验不经过,无权访问该资源!"); } log.info("权限验证经过"); // 设置用户信息到request里 request.setAttribute("id", claims.get("id")); request.setAttribute("userName", claims.get("userName")); request.setAttribute("role", claims.get("role")); return joinPoint.proceed(); } }
使用的时候只须要加上该注解而且设置角色名称便可,以下示例:
/** * 须要校验登陆态及权限后才能访问的资源 */ @GetMapping("/{id}") @CheckAuthorization("admin") public User findById(@PathVariable Integer id) { log.info("get request. id is {}", id); return userService.findById(id); }