Фронтенд и бэкенд реализуют проверку перехвата токена входа

задняя часть внешний интерфейс Angular.js RxJS
Фронтенд и бэкенд реализуют проверку перехвата токена входа

Сцена и окружение

1. Я начинающий веб-работник и каждый день беспокоюсь о своем будущем. Первый раз записываю процесс ежедневного развития, если нецензурно выражаетесь, прошу отшутиться;
2. Внешний интерфейс среды разработки в этом примере использует среду angular, а серверная часть использует среду Springboot;
3. Целью реализации является:
  a, интерфейс реализует операцию входа в систему (без функции регистрации);
Б. Задний конец получает информацию для входа в систему, генерирует токен с периодом действия (секретный ключ, генерируемый задним алгоритмом) и возвращает его на передний план в качестве результата;
  c. Каждый запрос от внешнего интерфейса будет содержать токен и проверять его с помощью внутреннего интерфейса;
г. Внешний запрос и ответ будут успешными в течение допустимого времени токена, а серверная часть обновит действительное время токена в режиме реального времени (еще нет реализации).Если токен недействителен, логин страница будет возвращена.

Во-вторых, внутренняя логика реализации

Примечание. Некоторые коды относятся к информации о различных великих богах в Интернете.
Вся структура серверного проекта выглядит следующим образом (перехват токена входа является лишь частью этого проекта, а адрес проекта будет вставлен в конце статьи):

1. Добавлена ​​модель класса AccessToken

  Добавьте AccessToken.java в файл модели, этот класс модели сохраняет информацию о токене проверки:

/**
 * @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. Добавьте модель класса аудитории

@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

Файл конфигурации определяет информацию о номере порта, корневом пути и полях, связанных с аудиторией (аудитория также называется в соответствии с онлайн-данными).Функция аудитории состоит в том, чтобы генерировать действительный токен при первом входе в систему, а затем сохранять информацию о токене в указанный выше AccessToken. В модели класса удобно проверять правильность информации о токене, переносимой внешним интерфейсом, после успешного входа в систему.

3. Создайте класс инструментов CreateTokenUtils в пакете jwt.

   Генерация и функции этого класса инструментов описаны ниже:
  a, сначала обратитесь к зависимостям в файле pom.xml (это похоже на интерфейсную установку пакетов npm в package.json)

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

B, затем добавьте класс инструмента CreateTokenUtils в папку UITLS, код выглядит следующим образом:

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 — этот метод используется во внутреннем перехватчике, чтобы определить, имеет ли запрос, отправленный внешним интерфейсом, значение токена.
 createJWT — этот метод вызывается в интерфейсе входа в систему, и значение токена генерируется для первого входа в систему.
 parseJWT — этот метод вызывается в checkJWT, анализирует значение токена и разлагает значение токена типа jwt на модули аудитории.
 Вы можете сломать точку в методе parseJWT, просмотреть объект Claims и обнаружить, что значение, хранящееся в его поле, соответствует значению объекта аудитории.
Примечание: объект Claims будет напрямую определять, истек ли срок действия токена, поэтому нет необходимости писать соответствующую логику сравнения времени. аудитория будет напрямую анализироваться объектом Claims

4. Реализация перехватчика Реализация класса HTTPBasicAuthorizeHandler

Создайте новый класс HTTPBasicAuthorizeHandler в папке typesHandlers со следующим кодом:

@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 и destroy, ключевая функция перехвата находится в методе doFitler:

  а. Запрос от фронтенда будет идти в этот метод, поэтому очевидно, что первый запрос на вход не должен быть перехвачен, потому что он не имеет значения токена, поэтому случай перехвата входа исключен:

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);

Определить, следует ли перехватывать в соответствии со значением allowPath;
 b. При перехвате вызовите метод checkJWT вышеуказанного класса инструментов, чтобы определить, действителен ли токен:

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

ReturnModel — это структура типа возвращаемого значения, которую я определил в файле модели;
  в. Если токен недействителен, код обработки аннотируется:

Причина в том, что перехватчик, реализованный внешним интерфейсом angular, будет конфликтовать с внутренним, что приведет к исключению кода внешнего интерфейса, которое будет подробно объяснено позже.
 d. Существует два способа настройки перехватчиков (здесь представлен только один):


Метод добавления аннотаций непосредственно в класс перехвата, urlPatterns — это путь, который вы фильтруете, и его нужно настроить там, где запускается служба


Примечание. Отфильтрованный путь здесь не включает корневой путь к файлу конфигурации, например путь интерфейса внешнего доступа «/movies/people/exist», где фильмы — это корневой путь, который настроен в файле конфигурации. , если вы хотите перехватить этот путь, то можно использовать urlPatterns="/people/exist".

5. Реализация класса входа

Создайте новый класс 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());

Наконец, интеграция возвращается к интерфейсной модели.

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);

В этом классе контроллера также есть два интерфейса, которые будут вызываться после успешного выполнения внешнего интерфейса.

Выше приведена логика реализации сервера.Далее я объясню логику реализации фронтенда.Я небольшой фермер кода фронтенда,но большая часть бэкенда не годится.Если есть какая-то ошибка , пожалуйста, передайте с улыбкой~_~ха

3. Логика реализации фронтенда

Интерфейс использует фреймворк angular, каталог выглядит следующим образом

В приведенном выше файле приложения common хранит некоторые общие компоненты (пейджинг, всплывающие окна), компонент хранит некоторые общие рамки макета, page — это каждый компонент страницы, service — это место сбора интерфейса запроса, а shared — это настраиваемая проверка формы; поэтому существуют различные логики реализации, связанные с проверкой формы angular2+, HTTP-запросом, пейджингом, угловой анимацией и так далее.

1. Внешний http-запрос (точнее httpClient-запрос)

Все запросы находятся в файле 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);
  /**首页模块结束 */
}

В бэкэнд-классе Persontroller в бэкэнд-классе Persontroller есть три метода интерфейса, это здесь не описывается, это .httpoptions — это заголовки настройки. Затем App.Modules.ts добавляется в Provides, так называемое внедрение зависимостей, так что вы можете вызывать сервисный метод на каждой странице.

 providers: [ServiceService,httpInterceptorProviders]

httpInterceptorProviders — это внешний перехватчик.Каждый раз, когда результат внешнего запроса будет успешным или ошибочным, поэтому унифицированная обработка возвращаемых результатов в перехватчике делает код более кратким.

2. Реализация внешнего перехватчика

Создайте новый файл 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');
       }
   }
}

Реализация перехватчика также подробно описана на официальном сайте, но у перехватчика есть несколько ям:
А. Если вы используете angular2, и ваш запрос состоит в том, чтобы использовать import { Http } из http-пакета «@angular/http», то перехватчик недействителен, вам может понадобиться другой способ записи, angular4, 5, 6 все используют импорт { HttpClient,HttpHeaders } из пакета "@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));
        }));

Прервите точку, чтобы просмотреть этот метод. Один запрос зациклится дважды, первое событие: {type: 0}, а второе вернет объект. Скриншот выглядит следующим образом: первый раз

второй раз

Но если я обрабатываю код в случае, когда вышеупомянутый токен внутреннего перехватчика недействителен (это код, который я аннотировал, основная функция аннотированного мной кода — вернуть 401, который можно просмотреть назад), это логика будет зацикливаться только один раз, поэтому я буду. Конечный код возвращает комментарий к коду с недопустимым токеном, а тело результата события, возвращаемое интерфейсным перехватчиком во второй раз, в случае внутреннего комментария к коду имеет событие. body=== null, и это условие используется для оценки того, действителен ли токен;
c. Перехватчик использует rxjs. Если вы используете метод Observable.forkJoin() в rxjs для одновременных запросов в запросе страницы, извините, он кажется недействительным. Если у вас есть способ решить конфликт между ними, дайте мне знать.
  d. Здесь также следует убрать перехват входа в систему, подробности см. в коде.

3. Эффект входа

Приведенная выше логика представляет собой процесс реализации Давайте посмотрим на общий эффект:
В логике входа я использую localStorage для хранения значения токена:

Нажатие на вход в систему сначала перейдет к интерфейсному перехватчику, а затем перейдет непосредственно к другому
Затем к перехватчику серверной службы
Отфильтровать интерфейс входа, перейти непосредственно к интерфейсу входа, создать значение токена и вернуться
Наблюдайте за возвращаемым значением карты
Наконец, вернитесь к внешнему интерфейсу
Возвращаемый выше результат соответствует серверной части. После успешного входа запрос других страниц будет содержать значение токена.

Вышеизложенное касается проверки входа в систему с разделением между интерфейсом и сервером.Есть еще один шаг, который еще не завершен, то есть срок действия времени обновления токена.Он будет добавлен по истечении времени.Редактор идей используется для серверной части приведенного выше кода, и создание серверной службы потребует много настроек.
Адрес github кода, реализованного выше, выглядит следующим образом:GitHub.com/ежемесячный брак…Пожалуйста, поставьте мне палец вверх, это первый раз, когда я пишу отчетный документ, я продолжу его писать и буду твердо верить, что он будет становиться все лучше и лучше, спасибо.