前后端如何實(shí)現(xiàn)登錄token攔截校驗(yàn)詳解
一、場(chǎng)景與環(huán)境
最近需要寫(xiě)一下前后端分離下的登錄解決方案,目前大多數(shù)都采用請(qǐng)求頭攜帶 Token 的形式
1、我是名小白web工作者,每天都為自己的將來(lái)?yè)?dān)心不已。第一次記錄日常開(kāi)發(fā)中的過(guò)程,如有表達(dá)不當(dāng),還請(qǐng)一笑而過(guò);
2、本實(shí)例開(kāi)發(fā)環(huán)境前端采用 angular框架,后端采用 springboot框架;
3、實(shí)現(xiàn)的目的如下:
a、前端實(shí)現(xiàn)登錄操作(無(wú)注冊(cè)功能);
b、后端接收到登錄信息,生成有效期限token(后端算法生成的一段秘鑰),作為結(jié)果返回給前端;
c、前端在此后的每次請(qǐng)求,都會(huì)攜帶token與后端校驗(yàn);
d、在token有效時(shí)間內(nèi)前端的請(qǐng)求響應(yīng)都會(huì)成功,后端實(shí)時(shí)的更新token有效時(shí)間(暫無(wú)實(shí)現(xiàn)),如果token失效則返回登錄頁(yè)。
二、后端實(shí)現(xiàn)邏輯
注:部分代碼參考網(wǎng)上各個(gè)大神的資料
整個(gè)服務(wù)端項(xiàng)目結(jié)構(gòu)如下(登錄token攔截只是在此工程下的一部分,文章結(jié)尾會(huì)貼上工程地址):

1、新增AccessToken 類 model
在model文件下新增AccessToken.java,此model 類保存校驗(yàn)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
配置文件定義了端口號(hào)、根路徑和audience相關(guān)字段的信息,(audience也是根據(jù)網(wǎng)上資料命名的),audience的功能主要在第一次登錄時(shí),生成有效token,然后將token的信息存入上述AccessToken類model中,方便登錄成功后校驗(yàn)前端攜帶的token信息是否正確。
3、生成以jwt包的CreateTokenUtils 工具類
下面對(duì)這個(gè)工具類的生成、功能進(jìn)行說(shuō)明:
a、首先在pom.xml文件中引用依賴(這和前端在package.json安裝npm包性質(zhì)相似)
<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();
}
}
此工具類有三個(gè) 靜態(tài)方法:
checkJWT—— 此方法在后端攔截器中使用,檢測(cè)前端發(fā)來(lái)的請(qǐng)求是否帶有token值
createJWT——此方法在登陸接口中調(diào)用,首次登陸生成token值
parseJWT——此方法在checkJWT中調(diào)用,解析token值,將jwt類型的token值分解成audience模塊
可以在parseJWT方法中打斷點(diǎn),查看Claims 對(duì)象,發(fā)現(xiàn)其字段存儲(chǔ)的值與audience對(duì)象值一一對(duì)應(yīng)。
注:Claims對(duì)象直接會(huì)將token的有效期進(jìn)行判斷是否過(guò)期,所以不需要再另寫(xiě)相關(guān)時(shí)間比對(duì)邏輯,前端的帶來(lái)的時(shí)間與后臺(tái)的配置文件audience的audience.expiresSecond=1800 Claims對(duì)象會(huì)直接解析
4、攔截器的實(shí)現(xiàn)HTTPBasicAuthorizeHandler類的實(shí)現(xiàn)
在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類,所以重寫(xiě)的三個(gè)方法init、doFitler、destory,重點(diǎn)攔截的功能在doFitler方法中:
a、前端發(fā)來(lái)請(qǐng)求都會(huì)到這個(gè)方法,那么顯而易見(jiàn),第一登陸請(qǐng)求肯定不能攔截,因?yàn)樗粠в衪oken值,所以剔除登錄攔截這種情況:
private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/person/exsit")));
這里面的我的登錄接口路徑是“/person/exsit”,所以在將前端請(qǐng)求路徑分解:
String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
兩者進(jìn)行如下比對(duì):
Boolean allowedPath = ALLOWED_PATHS.contains(path);
根據(jù)allowedPath 的值進(jìn)行判斷是否攔截;
b、攔截的時(shí)候調(diào)用上述工具類的checkJWT方法,判斷token是否有效:
ReturnModel returnModel = CreateTokenUtils.checkJWT((HttpServletRequest)servletRequest,audience.getBase64Secret());
ReturnModel 是我定義的返回類型結(jié)構(gòu),在model文件下;
c、如果token無(wú)效,處理代碼注釋了:

原因前端angular實(shí)現(xiàn)的攔截器和后端會(huì)沖突,導(dǎo)致前端代碼異常,后面會(huì)詳細(xì)說(shuō)明。
d、配置攔截器有兩種方法(這里只介紹一種):

直接在攔截類上添加注釋的方法,urlPatterns是你過(guò)濾的路徑,還需在服務(wù)啟動(dòng)的地方配置

注:這里面過(guò)濾的路徑不包括配置文件的根路徑,比如說(shuō)前端訪問(wèn)接口路徑“/movies/people/exist”,這里面的movies是根路徑,在配置文件中配置,如果你想攔截這個(gè)路徑,則urlPatterns=”/people/exist“即可。
5、登錄類的實(shí)現(xiàn)
在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:根據(jù)id對(duì)應(yīng)的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,"無(wú)此用戶");
}
}
}
前端調(diào)用這個(gè)類的接口路徑:“/movies/people/exist”
首先它會(huì)查詢數(shù)據(jù)庫(kù)
String id = personBll.getPersonExist(userName,md5PassWord);
如果查詢存在,創(chuàng)建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);
這個(gè)controller類中還有兩個(gè)接口供前端登陸成功后調(diào)用。
以上都是服務(wù)端的實(shí)現(xiàn)邏輯,接下來(lái)說(shuō)明前端的實(shí)現(xiàn)邏輯,我本身是前端小碼農(nóng),后端只是大多是不會(huì)的,如有錯(cuò)誤,請(qǐng)一笑而過(guò)哈~_~哈
三、前端實(shí)現(xiàn)邏輯
前端使用angular框架,目錄如下

上述app文件下common 存一些共同組建(分頁(yè)、彈框)、component存一些整體布局框架、
page是各個(gè)頁(yè)面組件,service是請(qǐng)求接口聚集地,shared是表單自定義校驗(yàn);所以這里面都有相關(guān)的angular2+表單校驗(yàn)、http請(qǐng)求、分頁(yè)、angular動(dòng)畫(huà)等各種實(shí)現(xiàn)邏輯。
1、前端http請(qǐng)求(確切的說(shuō)httpClient請(qǐng)求)
所有的請(qǐng)求都在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',
}),
}
}
/**登錄模塊開(kāi)始*/
loginMovies(body){
const url = this.movies+"/person/exsit";
const param = 'userName='+body.userName+"&passWord="+body.password;
return this.http.post(url,param,this.httpOptions);
}
/**登錄模塊結(jié)束*/
//首頁(yè);
getPersonItem(param){
const url = this.movies+"/person/item";
return this.http.get(url,{params:param});
}
//個(gè)人中心
getPersonList(){
const url = this.movies+"/person/list";
return this.http.get(url);
/**首頁(yè)模塊結(jié)束 */
}
上述有三個(gè)請(qǐng)求與后端personController類中三個(gè)接口方法一一對(duì)應(yīng),這里面的請(qǐng)求方式官網(wǎng)有,這里不做贅述,this.httpOptions是設(shè)置請(qǐng)求頭。然后再app.modules.ts中添加到provides,所謂的依賴注入,這樣就可以在各個(gè)頁(yè)面調(diào)用servcie方法了
providers: [ServiceService,httpInterceptorProviders]
httpInterceptorProviders 是前端攔截器,前端每次請(qǐng)求結(jié)果都會(huì)出現(xiàn)成功或者錯(cuò)誤,所以在攔截器中統(tǒng)一處理返回結(jié)果使代碼更簡(jiǎn)潔。
2、前端攔截器的實(shí)現(xiàn)
在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> {
// 業(yè)務(wù)處理:一些通用操作
switch (event.status) {
case 200:
if (event instanceof HttpResponse) {
const body: any = event.body;
if (body === null) {
this.backForLoginOut();
}
}
break;
case 401: // 未登錄狀態(tài)碼
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');
}
}
}
攔截器的實(shí)現(xiàn)官網(wǎng)也詳細(xì)說(shuō)明了,但是攔截器有幾大坑:
a、如果用的是angular2,你請(qǐng)求是采用的是import { Http } from "@angular/http"包http,那么攔截器無(wú)效,你可能需要另一種寫(xiě)法了,angular4、5、6都是采用import { HttpClient,HttpHeaders } from "@angular/common/http"包下HttpClient和請(qǐng)求頭HttpHeaders ;
b、攔截器返回結(jié)果的方法中:
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));
}));
打斷點(diǎn)查看這個(gè)方法一次請(qǐng)求會(huì)循環(huán)兩次,第一次event:{type:0} ,第二次才會(huì)返回對(duì)象,截圖如下:
第一次

第二次

但是如果以我上述后端攔截器token無(wú)效的情況處理代碼(就是我注釋的那段代碼,我注釋的代碼重點(diǎn)的作用是返回401,可以回看),這個(gè)邏輯只循環(huán)一次,所以我將后端代碼返回token無(wú)效的代碼注釋,前端攔截器在后端代碼注釋的情況下第二次返回的event結(jié)果體存在event.body=== null ,以這個(gè)條件進(jìn)行token是否有效判斷;
c、攔截器使用rxjs,如果你在頁(yè)面請(qǐng)求中使用rxjs中Observable.forkJoin()方法進(jìn)行并發(fā)請(qǐng)求,那么不好意思,好像無(wú)效,如果你有辦法解決這兩個(gè)不沖突,請(qǐng)告訴我哈。
d、這里面也要剔除登陸的攔截,具體看代碼。
3、登錄效果
以上的邏輯都是實(shí)現(xiàn)過(guò)程,下面來(lái)看下整體的效果:
登陸邏輯中我用的是localStorage存儲(chǔ)token值的:

點(diǎn)擊登錄會(huì)先到前端攔截器,然后直接跳到else


接著到后端服務(wù)攔截器

過(guò)濾登陸接口,直接跳到登陸接口,創(chuàng)建token值并返回

觀察返回的map值

最后返回前端界面

上面的返回結(jié)果與后端對(duì)應(yīng),登錄成功后,再請(qǐng)求其他頁(yè)面會(huì)攜帶token值

以上就是關(guān)于前后端分離登錄校驗(yàn),還有一步?jīng)]有完成,就是token更新時(shí)間有效期,等抽時(shí)間再補(bǔ)充,上述代碼后端用idea編輯器,后端服務(wù)搭建會(huì)涉及到很多配置。
上面實(shí)現(xiàn)的代碼github地址如下:github.com/yuelinghuny… (本地下載)
麻煩各位給我點(diǎn)個(gè)贊,第一次寫(xiě)記錄文檔,我會(huì)堅(jiān)持寫(xiě)下去,會(huì)堅(jiān)信越來(lái)越好,謝謝。
總結(jié):
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
詳解使用angularjs的ng-options時(shí)如何設(shè)置默認(rèn)值(初始值)
本篇文章主要介紹了詳解使用angularjs的ng-options時(shí)如何設(shè)置默認(rèn)值(初始值),具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
Angular中ng?update命令force參數(shù)含義詳解
這篇文章主要為大家介紹了Angular中ng?update命令force參數(shù)含義詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
angularjs+bootstrap實(shí)現(xiàn)自定義分頁(yè)的實(shí)例代碼
本篇文章主要介紹了angularjs+bootstrap實(shí)現(xiàn)自定義分頁(yè)的實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
詳解angularJS+Ionic移動(dòng)端圖片上傳的解決辦法
本篇文章主要介紹了詳解angularJS+Ionic移動(dòng)端圖片上傳的解決辦法 ,具有一定的參考價(jià)值,有需要的可以了解一下2017-09-09
詳解angularJs中關(guān)于ng-class的三種使用方式說(shuō)明
本篇文章主要介紹了angularJs中關(guān)于ng-class的三種使用方式說(shuō)明,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
詳解angularJs中自定義directive的數(shù)據(jù)交互
這篇文章主要介紹了詳解angularJs中自定義directive的數(shù)據(jù)交互,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01
Angular依賴注入optional?constructor?parameters概念
這篇文章主要為大家介紹了Angular?依賴注入領(lǐng)域里?optional?constructor?parameters?的概念及使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11

