一、我是名小白web工做者,天天都爲本身的未來擔憂不已。第一次記錄平常開發中的過程,若有表達不當,還請一笑而過;
二、本實例開發環境前端採用 angular框架,後端採用 springboot框架;
三、實現的目的以下:
a、前端實現登陸操做(無註冊功能);
b、後端接收到登陸信息,生成有效期限token(後端算法生成的一段祕鑰),做爲結果返回給前端;
c、前端在此後的每次請求,都會攜帶token與後端校驗;
d、在token有效時間內前端的請求響應都會成功,後端實時的更新token有效時間(暫無實現),若是token失效則返回登陸頁。前端
注:部分代碼參考網上各個大神的資料
整個服務端項目結構以下(登陸token攔截只是在此工程下的一部分,文章結尾會貼上工程地址):
java
在model文件下新增AccessToken.java,此model 類保存校驗token的信息:git
/**
* @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;
}
}
複製代碼
@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),以下:github
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信息是否正確。web
下面對這個工具類的生成、功能進行說明:
a、首先在pom.xml文件中引用依賴(這和前端在package.json安裝npm包性質類似)算法
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
複製代碼
b、而後再uitls文件夾下新增工具類CreateTokenUtils,代碼以下 :spring
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對象會直接解析數據庫
在typesHandlers文件夾中新建HTTPBasicAuthorizeHandler類,代碼以下:npm
@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方法中:json
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、配置攔截器有兩種方法(這裏只介紹一種):
在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框架,目錄以下
全部的請求都在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 是前端攔截器,前端每次請求結果都會出現成功或者錯誤,因此在攔截器中統一處理返回結果使代碼更簡潔。
在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,那麼攔截器無效,你可能須要另外一種寫法了,angular四、五、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},第二次纔會返回對象,截圖以下: 第一次
以上的邏輯都是實現過程,下面來看下總體的效果:
登錄邏輯中我用的是localStorage存儲token值的:
以上就是關於先後端分離登陸校驗,還有一步沒有完成,就是token更新時間有效期,等抽時間再補充,上述代碼後端用idea編輯器,後端服務搭建會涉及到不少配置。
上面實現的代碼github地址以下:github.com/yuelinghuny… 麻煩各位給我點個贊,第一次寫記錄文檔,我會堅持寫下去,會堅信愈來愈好,謝謝。