前后端如何实现登录token拦截校验详解


Posted in Javascript onSeptember 03, 2018

一、场景与环境

最近需要写一下前后端分离下的登录解决方案,目前大多数都采用请求头携带 Token 的形式

1、我是名小白web工作者,每天都为自己的将来担心不已。第一次记录日常开发中的过程,如有表达不当,还请一笑而过;

2、本实例开发环境前端采用 angular框架,后端采用 springboot框架;

3、实现的目的如下:

  a、前端实现登录操作(无注册功能);

  b、后端接收到登录信息,生成有效期限token(后端算法生成的一段秘钥),作为结果返回给前端;

  c、前端在此后的每次请求,都会携带token与后端校验;

  d、在token有效时间内前端的请求响应都会成功,后端实时的更新token有效时间(暂无实现),如果token失效则返回登录页。

二、后端实现逻辑

注:部分代码参考网上各个大神的资料

整个服务端项目结构如下(登录token拦截只是在此工程下的一部分,文章结尾会贴上工程地址):

前后端如何实现登录token拦截校验详解

1、新增AccessToken 类 model

  在model文件下新增AccessToken.java,此model 类保存校验token的信息:

/**
 * @param access_token token字段;
 * @param token_type token类型字段;
 * @param expires_in token 有效期字段;
 */
public class AccessToken {
 private String access_token;
 private String token_type;
 private long expires_in;

 public String getAccess_token() {
  return access_token;
 }

 public void setAccess_token(String access_token) {
  this.access_token = access_token;
 }

 public String getToken_type() {
  return token_type;
 }

 public void setToken_type(String token_type) {
  this.token_type = token_type;
 }

 public long getExpires_in() {
  return expires_in;
 }

 public void setExpires_in(long expires_in) {
  this.expires_in = expires_in;
 }
}

2、新增Audience 类 model

@ConfigurationProperties(prefix = "audience")
public class Audience {
 private String clientId;
 private String base64Secret;
 private String name;
 private int expiresSecond;

 public String getClientId() {
  return clientId;
 }

 public void setClientId(String clientId) {
  this.clientId = clientId;
 }

 public String getBase64Secret() {
  return base64Secret;
 }

 public void setBase64Secret(String base64Secret) {
  this.base64Secret = base64Secret;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public int getExpiresSecond() {
  return expiresSecond;
 }

 public void setExpiresSecond(int expiresSecond) {
  this.expiresSecond = expiresSecond;
 }
}

@ConfigurationProperties(prefix = "audience")获取配置文件的信息(application.properties),如下:

server.port=8888
spring.profiles.active=dev
server.servlet.context-path=/movies

audience.clientId=098f6bcd4621d373cade4e832627b4f6
audience.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
audience.name=xxx
audience.expiresSecond=1800

配置文件定义了端口号、根路径和audience相关字段的信息,(audience也是根据网上资料命名的),audience的功能主要在第一次登录时,生成有效token,然后将token的信息存入上述AccessToken类model中,方便登录成功后校验前端携带的token信息是否正确。

3、生成以jwt包的CreateTokenUtils 工具类

  下面对这个工具类的生成、功能进行说明:

  a、首先在pom.xml文件中引用依赖(这和前端在package.json安装npm包性质相似)

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.6.0</version>
 </dependency>

  b、然后再uitls文件夹下新增工具类CreateTokenUtils,代码如下 :

public class CreateTokenUtils {
 private static Logger logger = LoggerFactory.getLogger(CreateTokenUtils.class);

 /**
  *
  * @param request
  * @return s;
  * @throws Exception
  */
 public static ReturnModel checkJWT(HttpServletRequest request,String base64Secret)throws Exception{
  Boolean b = null;
  String auth = request.getHeader("Authorization");
  if((auth != null) && (auth.length() > 4)){
   String HeadStr = auth.substring(0,3).toLowerCase();
   if(HeadStr.compareTo("mso") == 0){
    auth = auth.substring(4,auth.length());
    logger.info("claims:"+parseJWT(auth,base64Secret));
    Claims claims = parseJWT(auth,base64Secret);
    b = claims==null?false:true;
   }
  }
  if(b == false){
   logger.error("getUserInfoByRequest:"+ auth);
   return new ReturnModel(-1,b);
  }
  return new ReturnModel(0,b);
 }

 public static Claims parseJWT(String jsonWebToken, String base64Security){
  try
  {
   Claims claims = Jwts.parser()
     .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
     .parseClaimsJws(jsonWebToken).getBody();
   return claims;
  }
  catch(Exception ex)
  {
   return null;
  }
 }
 public static String createJWT(String name,String audience, String issuer, long TTLMillis, String base64Security)
 {
  SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

  long nowMillis = System.currentTimeMillis();
  Date now = new Date(nowMillis);

  byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);
  Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

  JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
    .claim("unique_name", name)
    .setIssuer(issuer)
    .setAudience(audience)
    .signWith(signatureAlgorithm, signingKey);
  if (TTLMillis >= 0) {
   long expMillis = nowMillis + TTLMillis;
   Date exp = new Date(expMillis);
   builder.setExpiration(exp).setNotBefore(now);
  }
  return builder.compact();
 }
}

此工具类有三个 静态方法:

 checkJWT—— 此方法在后端拦截器中使用,检测前端发来的请求是否带有token值

 createJWT——此方法在登陆接口中调用,首次登陆生成token值

 parseJWT——此方法在checkJWT中调用,解析token值,将jwt类型的token值分解成audience模块

 可以在parseJWT方法中打断点,查看Claims 对象,发现其字段存储的值与audience对象值一一对应。

注:Claims对象直接会将token的有效期进行判断是否过期,所以不需要再另写相关时间比对逻辑,前端的带来的时间与后台的配置文件audience的audience.expiresSecond=1800 Claims对象会直接解析

4、拦截器的实现HTTPBasicAuthorizeHandler类的实现

在typesHandlers文件夹中新建HTTPBasicAuthorizeHandler类,代码如下:

@WebFilter(filterName = "basicFilter",urlPatterns = "/*")
public class HTTPBasicAuthorizeHandler implements Filter {
 private static Logger logger = LoggerFactory.getLogger(HTTPBasicAuthorizeHandler.class);
 private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/person/exsit")));
 @Autowired
 private Audience audience;
 @Override
 public void init(FilterConfig filterConfig) throws ServletException {
  logger.info("filter is init");
 }
 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  logger.info("filter is start");
  try {
   logger.info("audience:"+audience.getBase64Secret());
   HttpServletRequest request = (HttpServletRequest) servletRequest;
   HttpServletResponse response = (HttpServletResponse) servletResponse;
   String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
   logger.info("url:"+path);
   Boolean allowedPath = ALLOWED_PATHS.contains(path);
   if(allowedPath){
    filterChain.doFilter(servletRequest,servletResponse);
   }else {
    ReturnModel returnModel = CreateTokenUtils.checkJWT((HttpServletRequest)servletRequest,audience.getBase64Secret());
    if(returnModel.getCode() == 0){
     filterChain.doFilter(servletRequest,servletResponse);
    }else {
     // response.setCharacterEncoding("UTF-8");
//     response.setContentType("application/json; charset=utf-8");
//     response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
//     ReturnModel rm = new ReturnModel();
//     response.getWriter().print(rm);
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
 @Override
 public void destroy() {
  logger.info("filter is destroy");
 }
}

此类继承Filter类,所以重写的三个方法init、doFitler、destory,重点拦截的功能在doFitler方法中:

 a、前端发来请求都会到这个方法,那么显而易见,第一登陆请求肯定不能拦截,因为它不带有token值,所以剔除登录拦截这种情况:

private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/person/exsit")));

这里面的我的登录接口路径是“/person/exsit”,所以在将前端请求路径分解:

String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");

两者进行如下比对:

Boolean allowedPath = ALLOWED_PATHS.contains(path);

根据allowedPath 的值进行判断是否拦截;

 b、拦截的时候调用上述工具类的checkJWT方法,判断token是否有效:

ReturnModel returnModel = CreateTokenUtils.checkJWT((HttpServletRequest)servletRequest,audience.getBase64Secret());

ReturnModel 是我定义的返回类型结构,在model文件下;

 c、如果token无效,处理代码注释了:

前后端如何实现登录token拦截校验详解

原因前端angular实现的拦截器和后端会冲突,导致前端代码异常,后面会详细说明。

 d、配置拦截器有两种方法(这里只介绍一种):

前后端如何实现登录token拦截校验详解

直接在拦截类上添加注释的方法,urlPatterns是你过滤的路径,还需在服务启动的地方配置

前后端如何实现登录token拦截校验详解

注:这里面过滤的路径不包括配置文件的根路径,比如说前端访问接口路径“/movies/people/exist”,这里面的movies是根路径,在配置文件中配置,如果你想拦截这个路径,则urlPatterns=”/people/exist“即可。

5、登录类的实现

在controller文件夹中新建PersonController类,代码如下

/**
 * Created by jdj on 2018/4/23.
 */
@RestController
@RequestMapping("/person")
public class PersonController {
 private final static Logger logger = LoggerFactory.getLogger(PersonController.class);
 @Autowired
 private PersonBll personBll;
 @Autowired
 private Audience audience;
 /**
  * @content:根据id对应的person
  * @param id=1;
  * @return returnModel
  */
 @RequestMapping(value = "/exsit",method = RequestMethod.POST)
 public ReturnModel exsit(
   @RequestParam(value = "userName") String userName,
   @RequestParam(value = "passWord") String passWord
 ){
  String md5PassWord = Md5Utils.getMD5(passWord);
  String id = personBll.getPersonExist(userName,md5PassWord);
  if(id == null||id.length()<0){
   return new ReturnModel(-1,null);
  }else {
   Map<String,Object> map = new HashMap<>();
   Person person = personBll.getPerson(id);
   map.put("person",person);
   String accessToken = CreateTokenUtils
     .createJWT(userName,audience.getClientId(), audience.getName(),audience.getExpiresSecond() * 1000, audience.getBase64Secret());
   AccessToken accessTokenEntity = new AccessToken();
   accessTokenEntity.setAccess_token(accessToken);
   accessTokenEntity.setExpires_in(audience.getExpiresSecond());
   accessTokenEntity.setToken_type("bearer");
   map.put("accessToken",accessTokenEntity);
   return new ReturnModel(0,map);
  }
 }
 /**
  * @content:list
  * @param null;
  * @return returnModel
  */
 @RequestMapping(value = "/list",method = RequestMethod.GET)
 public ReturnModel list(){
  List<Person> list = personBll.selectAll();
  if(list.size()==0){
   return new ReturnModel(-1,null);
  }else {
   return new ReturnModel(0,list);
  }
 }

 @RequestMapping(value = "/item",method = RequestMethod.GET)
 public ReturnModel getItem(
   @RequestParam(value = "id") String id
 ){
  Person person = personBll.getPerson(id);
  if(person != null){
   return new ReturnModel(0,person);
  }else {
   return new ReturnModel(-1,"无此用户");
  }
 }
}

前端调用这个类的接口路径:“/movies/people/exist”

首先它会查询数据库

String id = personBll.getPersonExist(userName,md5PassWord);

如果查询存在,创建accessToken

String accessToken = CreateTokenUtils
 .createJWT(userName,audience.getClientId(), audience.getName(),audience.getExpiresSecond() * 1000, audience.getBase64Secret());

最后整合返回到前端model

AccessToken accessTokenEntity = new AccessToken();
   accessTokenEntity.setAccess_token(accessToken);
   accessTokenEntity.setExpires_in(audience.getExpiresSecond());
   accessTokenEntity.setToken_type("bearer");
   map.put("accessToken",accessTokenEntity);
   return new ReturnModel(0,map);

这个controller类中还有两个接口供前端登陆成功后调用。

以上都是服务端的实现逻辑,接下来说明前端的实现逻辑,我本身是前端小码农,后端只是大多是不会的,如有错误,请一笑而过哈~_~哈

三、前端实现逻辑

前端使用angular框架,目录如下

前后端如何实现登录token拦截校验详解

上述app文件下common 存一些共同组建(分页、弹框)、component存一些整体布局框架、
page是各个页面组件,service是请求接口聚集地,shared是表单自定义校验;所以这里面都有相关的angular2+表单校验、http请求、分页、angular动画等各种实现逻辑。

1、前端http请求(确切的说httpClient请求)

所有的请求都在service文件夹service.service.ts文件中,代码如下:

import { Injectable } from '@angular/core';
import { HttpClient,HttpHeaders } from "@angular/common/http";
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/forkJoin';

@Injectable()
export class ServiceService {
 movies:string;
 httpOptions:Object;
 constructor(public http:HttpClient) {
 this.movies = "/movies";
 this.httpOptions = {
  headers:new HttpHeaders({
  'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8',
  }),
 }
 }
 /**登录模块开始*/
 loginMovies(body){
 const url = this.movies+"/person/exsit";
 const param = 'userName='+body.userName+"&passWord="+body.password;
 return this.http.post(url,param,this.httpOptions);
 }
 /**登录模块结束*/
 //首页;
 getPersonItem(param){
 const url = this.movies+"/person/item";
 return this.http.get(url,{params:param});
 }
 //个人中心
 getPersonList(){
 const url = this.movies+"/person/list";
 return this.http.get(url);
 /**首页模块结束 */
}

上述有三个请求与后端personController类中三个接口方法一一对应,这里面的请求方式官网有,这里不做赘述,this.httpOptions是设置请求头。然后再app.modules.ts中添加到provides,所谓的依赖注入,这样就可以在各个页面调用servcie方法了

providers: [ServiceService,httpInterceptorProviders]

httpInterceptorProviders 是前端拦截器,前端每次请求结果都会出现成功或者错误,所以在拦截器中统一处理返回结果使代码更简洁。

2、前端拦截器的实现

在app文件在新建InterceptorService.ts文件,代码如下:

import { Injectable } from '@angular/core';
import { HttpEvent,HttpInterceptor,HttpHandler,HttpRequest,HttpResponse} from "@angular/common/http";
import {Observable} from "rxjs/Observable";
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { mergeMap } from 'rxjs/operators';
import {Router} from '@angular/router';

@Injectable()
export class InterceptorService implements HttpInterceptor{
 constructor(
  private router:Router,
 ){ }
 authorization:string = "";
 authReq:any;
 intercept(req:HttpRequest<any>,next:HttpHandler):Observable<HttpEvent<any>>{
  this.authorization = "mso " + localStorage.getItem("accessToken");
  
  if (req.url.indexOf('/person/exsit') === -1) {
   this.authReq = req.clone({
    url:req.url,
    headers:req.headers.set("Authorization",this.authorization)
   });
  }else{
   this.authReq = req.clone({
    url:req.url,
   });
  }
  return next.handle(this.authReq).pipe(mergeMap((event:any) => {
   if(event instanceof HttpResponse && event.body === null){
    return this.handleData(event);
   }
   return Observable.create(observer => observer.next(event));
  }));
 }
 private handleData(event: HttpResponse<any>): Observable<any> {
  // 业务处理:一些通用操作
  switch (event.status) {
   case 200:
   if (event instanceof HttpResponse) {
    const body: any = event.body;
    if (body === null) {
     this.backForLoginOut();
    }
   }
   break;
   case 401: // 未登录状态码
   this.backForLoginOut();
   break;
   case 404:
   case 500:
   break;
   default:
   return ErrorObservable.create(event);
  }
 }
 private backForLoginOut(){
  if(localStorage.getItem("accessToken") !== null || localStorage.getItem("person")!== null){
   localStorage.removeItem("accessToken");
   localStorage.removeItem("person");
  }
   if(localStorage.getItem("accessToken") === null && localStorage.getItem("person") === null){
   this.router.navigateByUrl('/login');
  }
 }
}

拦截器的实现官网也详细说明了,但是拦截器有几大坑:

  a、如果用的是angular2,你请求是采用的是import { Http } from "@angular/http"包http,那么拦截器无效,你可能需要另一种写法了,angular4、5、6都是采用import { HttpClient,HttpHeaders } from "@angular/common/http"包下HttpClient和请求头HttpHeaders ;

  b、拦截器返回结果的方法中:

return next.handle(this.authReq).pipe(mergeMap((event:any) => {
   if(event instanceof HttpResponse && event.body === null){
    return this.handleData(event);
   }
   return Observable.create(observer => observer.next(event));
  }));

打断点查看这个方法一次请求会循环两次,第一次event:{type:0} ,第二次才会返回对象,截图如下:

第一次

前后端如何实现登录token拦截校验详解

第二次

前后端如何实现登录token拦截校验详解

但是如果以我上述后端拦截器token无效的情况处理代码(就是我注释的那段代码,我注释的代码重点的作用是返回401,可以回看),这个逻辑只循环一次,所以我将后端代码返回token无效的代码注释,前端拦截器在后端代码注释的情况下第二次返回的event结果体存在event.body=== null ,以这个条件进行token是否有效判断;

  c、拦截器使用rxjs,如果你在页面请求中使用rxjs中Observable.forkJoin()方法进行并发请求,那么不好意思,好像无效,如果你有办法解决这两个不冲突,请告诉我哈。

  d、这里面也要剔除登陆的拦截,具体看代码。

3、登录效果

以上的逻辑都是实现过程,下面来看下整体的效果:

登陆逻辑中我用的是localStorage存储token值的:

前后端如何实现登录token拦截校验详解

点击登录会先到前端拦截器,然后直接跳到else

前后端如何实现登录token拦截校验详解

前后端如何实现登录token拦截校验详解

接着到后端服务拦截器

前后端如何实现登录token拦截校验详解

过滤登陆接口,直接跳到登陆接口,创建token值并返回

前后端如何实现登录token拦截校验详解

观察返回的map值

前后端如何实现登录token拦截校验详解

最后返回前端界面

前后端如何实现登录token拦截校验详解

上面的返回结果与后端对应,登录成功后,再请求其他页面会携带token值

前后端如何实现登录token拦截校验详解

以上就是关于前后端分离登录校验,还有一步没有完成,就是token更新时间有效期,等抽时间再补充,上述代码后端用idea编辑器,后端服务搭建会涉及到很多配置。

上面实现的代码github地址如下:github.com/yuelinghuny… (本地下载)

麻烦各位给我点个赞,第一次写记录文档,我会坚持写下去,会坚信越来越好,谢谢。

总结:

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Jquery实现仿新浪微博获取文本框能输入的字数代码
Feb 22 Javascript
JavaScript中的eval()函数使用介绍
Dec 31 Javascript
JavaScript随机生成信用卡卡号的方法
Apr 07 Javascript
JavaScript DOM进阶方法
Apr 13 Javascript
利用JavaScript的AngularJS库制作电子名片的方法
Jun 18 Javascript
JavaScript实现设计模式中的单例模式的一些技巧总结
May 17 Javascript
不同js异步函数同步的实现方法
May 28 Javascript
js原生之焦点图转换加定时器实例
Dec 12 Javascript
Vue.js devtool插件安装后无法使用的解决办法
Nov 27 Javascript
微信小程序封装的HTTP请求示例【附升级版】
May 11 Javascript
ES6基础之 Promise 对象用法实例详解
Aug 22 Javascript
微信小程序实现分页加载效果
Nov 19 Javascript
vue移动端监听滚动条高度的实现方法
Sep 03 #Javascript
vue 纯js监听滚动条到底部的实例讲解
Sep 03 #Javascript
解决vue.js this.$router.push无效的问题
Sep 03 #Javascript
JavaScript封装的常用工具类库bee.js用法详解【经典类库】
Sep 03 #Javascript
基于vue-router 多级路由redirect 重定向的问题
Sep 03 #Javascript
vue-cli 引入jQuery,Bootstrap,popper的方法
Sep 03 #jQuery
Vue-router的使用和出现空白页,路由对象属性详解
Sep 03 #Javascript
You might like
php读取javascript设置的cookies的代码
2010/04/12 PHP
PHP 杂谈《重构-改善既有代码的设计》之四 简化条件表达式
2012/04/09 PHP
利用PHP获取网站访客的所在地位置
2017/01/18 PHP
php使用环形链表解决约瑟夫问题完整示例
2018/08/07 PHP
laravel框架使用FormRequest进行表单验证,验证异常返回JSON操作示例
2020/02/18 PHP
jQuery LigerUI 使用教程入门篇
2012/01/18 Javascript
js修改table中Td的值(定义td的双击事件)
2013/01/10 Javascript
JS将秒换成时分秒实现代码
2013/09/03 Javascript
js切换光标示例代码
2013/10/10 Javascript
javascript将相对路径转绝对路径示例
2014/03/14 Javascript
使用JSON.parse将json字符串转换成json对象的时候会出错
2014/09/04 Javascript
创建js对象和js类的方法汇总
2014/12/24 Javascript
javascript性能优化之DOM交互操作实例分析
2015/12/12 Javascript
jQuery实现漂亮实用的商品图片tips提示框效果(无图片箭头+阴影)
2016/04/16 Javascript
jQuery插件EasyUI实现Layout框架页面中弹出窗体到最顶层效果(穿越iframe)
2016/08/05 Javascript
js实现加载更多功能实例
2016/10/27 Javascript
Bootstrap源码学习笔记之bootstrap进度条
2016/12/24 Javascript
Restify中接入Socket.io报Error:Can’t set headers的错误解决
2017/03/28 Javascript
简单实现JS上传图片预览功能
2017/04/14 Javascript
vue父组件向子组件(props)传递数据的方法
2018/01/02 Javascript
详解webpack 入门与解析
2018/04/09 Javascript
JS字典Dictionary类定义与用法示例
2019/02/01 Javascript
JS内置对象和Math对象知识点详解
2020/04/03 Javascript
TensorFlow搭建神经网络最佳实践
2018/03/09 Python
python 3.7.0 安装配置方法图文教程
2018/08/27 Python
WxPython实现无边框界面
2019/11/18 Python
python+selenium+chromedriver实现爬虫示例代码
2020/04/10 Python
python中sympy库求常微分方程的用法
2020/04/28 Python
详解Django中views数据查询使用locals()函数进行优化
2020/08/24 Python
python归并排序算法过程实例讲解
2020/11/04 Python
进步之星获奖感言
2014/02/22 职场文书
网络编辑职责
2014/03/01 职场文书
法律进机关实施方案
2014/03/12 职场文书
教师个人自我评价
2015/03/04 职场文书
致地震灾区的慰问信
2015/03/23 职场文书
python控制台打印log输出重复的解决方法
2021/05/14 Python