这个模块分离至项目api权限管理系统与先后端分离实践,感受那样太长了找不到重点,分离出来要好点。html
对于登陆的用户签发其对应的jwt,咱们在jwt设置他的固定有效期时间,在有效期内用户携带jwt访问没问题,当过有效期后jwt失效,用户须要从新登陆获取新的jwt。这个体验不太好,好的体验应该是:活跃的用户应该在无感知的状况下在jwt失效后获取到新的jwt,携带这个新的jwt进行访问,而长时间不活跃的用户应该在jwt失效后须要进行从新的登陆认证。 前端
这里就涉及到了token的超时刷新问题,解决方案看图: java
在签发有效期为 t 时间的jwt后,把jwt用("JWT-SESSION-"+appId,jwt)的key-value形式存储到redis中,有效期设置为2倍的 t 。这样jwt在有效期事后的 t 时间段内能够申请刷新token。
还有个问题是用户携带过时的jwt对后台请求,在可刷新时间段内返回了新的jwt,应该在用户无感知的状况下返回请求的内容,而不是接收一个刷新的jwt。咱们是否是能够在每次request请求回调的时候判断返回的是否是刷新jwt,可是判断是以后咱们是否放弃以前的用户请求,若是不放弃,那是否是应该在最开始的用户request请求前先保存这个请求,在以后的回调中若是是返回刷新jwt,咱们再携带这个新的jwt再请求一次保存好的request请求?但对于前端这么大量的不一样请求,这样是否是太麻烦了? git
这困扰了我好久哎,直到我用到了angualr的HttpInterceptor
哈哈哈哈哈哈哈哈哈哈哈哈哈哈。 github
angualr的HttpInterceptor
就是前端的拦截过滤器,发起请求会拦截处理,接收请求也会拦截处理。最大的好处对每次的原始request他都会完整的保存下来,咱们向后台发生的request是他的clone。next.handle(request.clone)
继承HttpInterceptor的AuthInterceptor,拦截response判断是否为refresh token,是则携带新token再次发起保存的request:redis
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private router: Router) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authToken = this.authService.getAuthorizationToken(); const uid = this.authService.getUid(); let authReq: any; if (authToken != null && uid != null) { authReq = req.clone({ setHeaders: { 'authorization': authToken, 'appId': uid } }); } else { authReq = req.clone(); } console.log(authReq); return next.handle(authReq).pipe( mergeMap(event => { // 返回response if (event instanceof HttpResponse) { if (event.status === 200) { // 若返回JWT过时但refresh token未过时,返回新的JWT 状态码为1005 if (event.body.meta.code === 1005) { const jwt = event.body.data.jwt; // 更新AuthorizationToken this.authService.updateAuthorizationToken(jwt); // clone request 从新发起请求 // retry(1); authReq = req.clone({ setHeaders: { 'authorization': jwt, 'appId': uid } }); return next.handle(authReq); } } if (event.status === 404) { // go to 404 html this.router.navigateByUrl('/404'); } if (event.status === 500) { // go to 500 html this.router.navigateByUrl('/500'); } } console.log(event); // 返回正常状况的可观察对象 return of(event); }), catchError(this.handleError) ); } private handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', error.error.message); } else { console.error( `Backend returned code ${error.status}, ` + `body was: ${error.error}`); } repeat(1); return new ErrorObservable('亲请检查网络'); } }
后端签发jwt时所作的:segmentfault
/* * * @Description 这里已经在 passwordFilter 进行了登陆认证 * @Param [] 登陆签发 JWT * @Return java.lang.String */ @ApiOperation(value = "用户登陆",notes = "POST用户登陆签发JWT") @PostMapping("/login") public Message accountLogin(HttpServletRequest request, HttpServletResponse response) { Map<String,String> params = RequestResponseUtil.getRequestParameters(request); String appId = params.get("appId"); // 根据appId获取其对应所拥有的角色(这里设计为角色对应资源,没有权限对应资源) String roles = accountService.loadAccountRole(appId); // 时间以秒计算,token有效刷新时间是token有效过时时间的2倍 long refreshPeriodTime = 36000L; String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId, "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512); // 将签发的JWT存储到Redis: {JWT-SESSION-{appID} , jwt} redisTemplate.opsForValue().set("JWT-SESSION-"+appId,jwt,refreshPeriodTime, TimeUnit.SECONDS); AuthUser authUser = userService.getUserByAppId(appId); return new Message().ok(1003,"issue jwt success").addData("jwt",jwt).addData("user",authUser); }
后端refresh token时所作的:后端
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception { Subject subject = getSubject(servletRequest,servletResponse); // 判断是否为JWT认证请求 if ((null == subject || !subject.isAuthenticated()) && isJwtSubmission(servletRequest)) { AuthenticationToken token = createJwtToken(servletRequest); try { subject.login(token); // return this.checkRoles(subject,mappedValue) && this.checkPerms(subject,mappedValue); return this.checkRoles(subject,mappedValue); }catch (AuthenticationException e) { LOGGER.info(e.getMessage(),e); // 若是是JWT过时 if (e.getMessage().equals("expiredJwt")) { // 这里初始方案先抛出令牌过时,以后设计为在Redis中查询当前appId对应令牌,其设置的过时时间是JWT的两倍,此做为JWT的refresh时间 // 当JWT的有效时间过时后,查询其refresh时间,refresh时间有效即从新派发新的JWT给客户端, // refresh也过时则告知客户端JWT时间过时从新认证 // 当存储在redis的JWT没有过时,即refresh time 没有过时 String appId = WebUtils.toHttp(servletRequest).getHeader("appId"); String jwt = WebUtils.toHttp(servletRequest).getHeader("authorization"); String refreshJwt = redisTemplate.opsForValue().get("JWT-SESSION-"+appId); if (null != refreshJwt && refreshJwt.equals(jwt)) { // 从新申请新的JWT // 根据appId获取其对应所拥有的角色(这里设计为角色对应资源,没有权限对应资源) String roles = accountService.loadAccountRole(appId); long refreshPeriodTime = 36000L; //seconds为单位,10 hours String newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId, "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512); // 将签发的JWT存储到Redis: {JWT-SESSION-{appID} , jwt} redisTemplate.opsForValue().set("JWT-SESSION-"+appId,newJwt,refreshPeriodTime, TimeUnit.SECONDS); Message message = new Message().ok(1005,"new jwt").addData("jwt",newJwt); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }else { // jwt时间失效过时,jwt refresh time失效 返回jwt过时客户端从新登陆 Message message = new Message().error(1006,"expired jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } } // 其余的判断为JWT错误无效 Message message = new Message().error(1007,"error Jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }catch (Exception e) { // 其余错误 LOGGER.warn(servletRequest.getRemoteAddr()+"JWT认证"+e.getMessage(),e); // 告知客户端JWT错误1005,需从新登陆申请jwt Message message = new Message().error(1007,"error jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } }else { // 请求未携带jwt 判断为无效请求 Message message = new Message().error(1111,"error request"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } }
持续更新。。。。。。
分享一波阿里云代金券快速上云
转载请注明 from tomsun28