有状态 VS 无状态
有状态是后台session存储用户信息,每次前台如果从一个浏览器访问的同一个服务器,就会解析cookie携带的sessionID从而解析到用户实现登录状态。
无状态是后台不再使用session,而是使用一串编码过的字符串传递到前台,前台使用某种技术进行保存,每次会话都要携带token向后台发送请求 ,后台通过解析token分别身份(也可以将特定的身份保存在redis中),从而实现无状态的登录。
认证方案
- 使用SpringCloud Security等框架
- 使用网关、颁发、解密、认证,认证成功后微服务内部不再认证
- 网关不认证,有专门的微服务进行token颁发、每个微服务在请求来到时都各自解析一遍token是否合法
- 将token存放在指定的后台服务器中,每次请求来的时候验证,同最初使用session是一样的。适合于旧项目改造新项目使用。
访问控制模型
- Access Control List (ACL)
- Role-based access control (RBAC)
- Attribute-based access control(ABAC)
- Rule-based access control
- Time-based access control
Role-based access control 用户 --(关联)-- 角色 --(关联)-- 权限
JWT
AOP实现用户登录检查
笔者之前使用的是拦截器进行登录校验,因为SpringAOP是Spring比较重要的一环,所以这里再次使用SpringAOP进行登录校验。
引入spring-boot-starter-aop
依赖,创建自定义注解,创建一个切面使用其中的方法匹配注解并编写检查登录逻辑。
package com.itmuch.contentcenter.auth;
public @interface CheckLogin {
}
package com.itmuch.contentcenter.auth;
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthAspect {
private final JwtOperator jwtOperator;
@Around("@annotation(com.itmuch.contentcenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
checkToken();
return point.proceed();
}
private void checkToken() {
try {
// 1. 从header里面获取token
HttpServletRequest request = getHttpServletRequest();
String token = request.getHeader("X-Token");
// 2. 校验token是否合法&是否过期;如果不合法或已过期直接抛异常;如果合法放行
Boolean isValid = jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token不合法!");
}
// 3. 如果校验成功,那么就将用户的信息设置到request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id", claims.get("id"));
request.setAttribute("wxNickname", claims.get("wxNickname"));
request.setAttribute("role", claims.get("role"));
} catch (Throwable throwable) {
throw new SecurityException("Token不合法");
}
}
//获取request
private HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
return attributes.getRequest();
}
@Around("@annotation(com.itmuch.contentcenter.auth.CheckAuthorization)")
public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
try {
// 1. 验证token是否合法;
this.checkToken();
// 2. 验证用户角色是否匹配
HttpServletRequest request = getHttpServletRequest();
String role = (String) request.getAttribute("role");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
String value = annotation.value();
if (!Objects.equals(role, value)) {
throw new SecurityException("用户无权访问!");
}
} catch (Throwable throwable) {
throw new SecurityException("用户无权访问!", throwable);
}
return point.proceed();
}
}
Feign实现token传递
- @RequestHeader 在服务提供者以及Feign的调用方的Controller方法参数列表中中全部加上
@RequestHeader("token") String token
。 - 使用RequestInterceptor
public class TokenRelayRequestIntecepor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 1. 获取到token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
// 2. 将token传递
if (StringUtils.isNotBlank(token)) {
template.header("X-Token", token);
}
}
}
这个配置在2-9一节中有提到
requestInterceptor:
- com.itmuch.contentcenter.feignclient.interceptor;
RestTemplate传递token
- exchange()
@GetMapping("/tokenRelay/{userId}")
public ResponseEntity<UserDTO> tokenRelay(@PathVariable Integer userId, HttpServletRequest request) {
String token = request.getHeader("X-Token");
HttpHeaders headers = new HttpHeaders();
headers.add("X-Token", token);
return this.restTemplate
.exchange(
"http://user-center/users/{userId}",
HttpMethod.GET,
new HttpEntity<>(headers),
UserDTO.class,
userId
);
}
- 实现ClientHttpRequestInterceptor
这个类是RestTemplate的拦截器,和Feign的差不多
public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest httpRequest = attributes.getRequest();
String token = httpRequest.getHeader("X-Token");
HttpHeaders headers = request.getHeaders();
headers.add("X-Token", token);
// 保证请求继续执行
return execution.execute(request, body);
}
}
还需要创建RestTemplate的时候进行拦截器的设置
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.setInterceptors(
Collections.singletonList(
new TestRestTemplateTokenRelayInterceptor()
)
);
return template;
}
AOP实现权限校验
使用自定义注解以及AOP的方式实现,同上文的检查token差不多
//这里表示在运行期注入注解
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
String value();
}
@PutMapping("/audit/{id}")
@CheckAuthorization("admin")
public Share auditById(@PathVariable Integer id, @RequestBody ShareAuditDTO auditDTO) {
return this.shareService.auditById(id, auditDTO);
}
@Around("@annotation(com.itmuch.contentcenter.auth.CheckAuthorization)")
public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
try {
// 1. 验证token是否合法;
this.checkToken();
// 2. 验证用户角色是否匹配
HttpServletRequest request = getHttpServletRequest();
String role = (String) request.getAttribute("role");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
String value = annotation.value();
if (!Objects.equals(role, value)) {
throw new SecurityException("用户无权访问!");
}
} catch (Throwable throwable) {
throw new SecurityException("用户无权访问!", throwable);
}
return point.proceed();
}