当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > 前后端如何实现登录token拦截校验详解

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

2018年09月21日  | 移动技术网IT编程  | 我要评论

一、场景与环境

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

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

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

3、实现的目的如下:

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

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

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

  d、在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无效,处理代码注释了:

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

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

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

注:这里面过滤的路径不包括配置文件的根路径,比如说前端访问接口路径“/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框架,目录如下

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

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

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

3、登录效果

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

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

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


接着到后端服务拦截器

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

观察返回的map值

最后返回前端界面

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

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

上面实现的代码github地址如下: ()

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

总结:

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网