一、场景与环境
最近需要写一下前后端分离下的登录解决方案,目前大多数都采用请求头携带 Token 的形式
1、我是名小白web工作者,每天都为自己的将来担心不已。第一次记录日常开发中的过程,如有表达不当,还请一笑而过;
2、本实例开发环境前端采用 angular框架,后端采用 springboot框架;
3、实现的目的如下:
"color: #ff0000">二、后端实现逻辑
注:部分代码参考网上各个大神的资料
整个服务端项目结构如下(登录token拦截只是在此工程下的一部分,文章结尾会贴上工程地址):
1、新增AccessToken 类 model
"htmlcode">
/** * @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 工具类
"htmlcode">
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.6.0</version> </dependency>
"htmlcode">
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"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(); } }
此工具类有三个 静态方法:
"color: #800000">注: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方法中:
"htmlcode">
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 的值进行判断是否拦截;
"htmlcode">
ReturnModel returnModel = CreateTokenUtils.checkJWT((HttpServletRequest)servletRequest,audience.getBase64Secret());
ReturnModel 是我定义的返回类型结构,在model文件下;
原因前端angular实现的拦截器和后端会冲突,导致前端代码异常,后面会详细说明。
直接在拦截类上添加注释的方法,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'); } } }
拦截器的实现官网也详细说明了,但是拦截器有几大坑:
"@angular/http"包http,那么拦截器无效,你可能需要另一种写法了,angular4、5、6都是采用import { HttpClient,HttpHeaders } from "@angular/common/http"
包下HttpClient和请求头HttpHeaders ;
"htmlcode">
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是否有效判断;
点击登录会先到前端拦截器,然后直接跳到else
接着到后端服务拦截器
过滤登陆接口,直接跳到登陆接口,创建token值并返回
观察返回的map值
最后返回前端界面
上面的返回结果与后端对应,登录成功后,再请求其他页面会携带token值
以上就是关于前后端分离登录校验,还有一步没有完成,就是token更新时间有效期,等抽时间再补充,上述代码后端用idea编辑器,后端服务搭建会涉及到很多配置。
上面实现的代码github地址如下:github.com/yuelinghuny… (本地下载)
麻烦各位给我点个赞,第一次写记录文档,我会坚持写下去,会坚信越来越好,谢谢。
总结:
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 好薇2024《兵哥哥》1:124K黄金母盘[WAV+CUE]
- 胡歌.2006-珍惜(EP)【步升大风】【FLAC分轨】
- 洪荣宏.2014-拼乎自己看【华特】【WAV+CUE】
- 伊能静.1999-从脆弱到勇敢1987-1996精选2CD【华纳】【WAV+CUE】
- 刘亮鹭《汽车DJ玩主》[WAV+CUE][1.1G]
- 张杰《最接近天堂的地方》天娱传媒[WAV+CUE][1.1G]
- 群星《2022年度发烧天碟》无损黑胶碟 2CD[WAV+CUE][1.4G]
- 罗文1983-罗文甄妮-射雕英雄传(纯银AMCD)[WAV+CUE]
- 群星《亚洲故事香港纯弦》雨果UPMAGCD2024[低速原抓WAV+CUE]
- 群星《经典咏流传》限量1:1母盘直刻[低速原抓WAV+CUE]
- 庾澄庆1993《老实情歌》福茂唱片[WAV+CUE][1G]
- 许巍《在别处》美卡首版[WAV+CUE][1G]
- 林子祥《单手拍掌》华纳香港版[WAV+CUE][1G]
- 郑秀文.1997-我们的主题曲【华纳】【WAV+CUE】
- 群星.2001-生命因爱动听电影原创音乐AVCD【MEDIA】【WAV+CUE】