Сигурността е враг на удобството и обратно. Това твърдение е вярно за всяка система, виртуална или реална, от физическия дом до платформи за уеб банкиране. Инженерите непрекъснато се опитват да намерят правилния баланс за дадения случай на употреба, накланяйки се на едната или другата страна. Обикновено, когато се появи нова заплаха, ние се придвижваме към сигурността и далеч от удобството. След това виждаме дали можем да възстановим малко загубено удобство, без да намаляваме твърде много сигурността. Освен това този порочен кръг продължава вечно.
Нека се опитаме да разгледаме състоянието на REST сигурността днес, като използваме ясен урок за пролетна защита, за да го демонстрираме в действие.
Услугите REST (което означава „Представителен държавен трансфер“) започнаха като изключително опростен подход към уеб услугите, който имаше огромни спецификации и тромави формати, като например WSDL за описание на услугата, или САПУН за определяне на формата на съобщението. В REST нямаме нито едно от тях. Можем да опишем услугата REST в обикновен текстов файл и да използваме всеки формат на съобщението, който искаме, като JSON, XML или дори обикновен текст отново. Опростеният подход беше приложен и към сигурността на REST услугите; нито един дефиниран стандарт не налага определен начин за удостоверяване на потребителите.
Въпреки че REST услугите не са много конкретизирани, важна е липсата на държава. Това означава, че сървърът не поддържа клиентско състояние, като сесиите са добър пример. По този начин сървърът отговаря на всяка заявка, сякаш е първата, направена от клиента. Въпреки това, дори и сега много реализации все още използват удостоверяване, базирано на бисквитки, което е наследено от стандартния архитектурен дизайн на уебсайта. Подходът на REST без гражданство прави бисквитките на сесията неподходящи от гледна точка на сигурността, но въпреки това те все още се използват широко. Освен игнорирането на необходимото безгражданство, опростеният подход дойде като очакван компромис за сигурността. В сравнение със стандарта WS-Security, използван за уеб услуги, е много по-лесно да създавате и консумирате REST услуги, следователно удобството премина през покрива. Компромисът е доста тънка сигурност; отвличането на сесии и фалшифицирането на заявки между сайтове (XSRF) са най-често срещаните проблеми със сигурността.
Опитвайки се да се отървете от клиентските сесии от сървъра, понякога се използват някои други методи, като Basic или Digest HTTP удостоверяване. И двамата използват Authorization
заглавка за предаване на потребителски идентификационни данни, с добавено малко кодиране (HTTP Basic) или криптиране (HTTP Digest). Разбира се, те имаха същите недостатъци, открити в уебсайтовете: HTTP Basic трябваше да се използва през HTTPS, тъй като потребителското име и паролата се изпращат в лесно обратимо кодиране base64, а HTTP Digest принуди използването на остаряло хеширане на MD5, което е доказано несигурно.
И накрая, някои реализации са използвали произволни маркери за удостоверяване на клиенти. Засега тази опция изглежда е най-добрата, която имаме. Ако се приложи правилно, той коригира всички проблеми със сигурността на HTTP Basic, HTTP Digest или бисквитките на сесията, той е лесен за използване и следва модела без гражданство.
При такива произволни жетони обаче има малко включен стандарт. Всеки доставчик на услуги имаше своя идея какво да постави в маркера и как да го кодира или кодира. Потреблението на услуги от различни доставчици изисква допълнително време за настройка, само за да се адаптира към конкретния използван формат на маркера. Другите методи, от друга страна (сесийна бисквитка, HTTP Basic и HTTP Digest), са добре известни на разработчиците и почти всички браузъри на всички устройства работят с тях нестандартно. Рамките и езиците са готови за тези методи, като имат вградени функции, за да се справят безпроблемно с всеки.
миграция на данни от наследени системи
JWT (съкратено от JSON Web Token) е липсващата стандартизация за използване на маркери за удостоверяване в мрежата като цяло, не само за REST услуги. В момента той е в статут на чернови като RFC 7519 . Той е здрав и може да носи много информация, но все пак е лесен за използване, въпреки че размерът му е сравнително малък. Както всеки друг маркер, JWT може да се използва за предаване на самоличността на удостоверени потребители между доставчик на идентичност и доставчик на услуги (които не са непременно същите системи). Той може също да носи всички претенции на потребителя, като например данни за оторизация, така че доставчикът на услуги не е необходимо да влиза в базата данни или външни системи, за да проверява потребителските роли и разрешения за всяка заявка; че данните се извличат от маркера.
Ето как е проектирана защитата на JWT:
Authorization
header и го декриптира, ако е необходимо, потвърждава подписа и ако всичко е наред, извлича потребителските данни и разрешения. Въз основа само на тези данни и отново, без да търси допълнителни подробности в базата данни или да се свързва с доставчика на идентичност, той може да приеме или откаже заявката на клиента. Единственото изискване е доставчиците на самоличност и услуги да имат споразумение за криптиране, така че услугата да може да провери подписа или дори да дешифрира коя самоличност е криптирана.Този поток позволява голяма гъвкавост, като същевременно запазва нещата сигурни и лесни за развитие. Използвайки този подход, е лесно да добавите нови сървърни възли към клъстера на доставчика на услуги, като ги инициализирате само с възможност за проверка на подписа и декриптиране на маркерите, като им предоставите споделен таен ключ. Не се изисква репликация на сесия, синхронизиране на база данни или комуникация между възли. ПОЧИВЕТЕ в пълния си блясък.
Основната разлика между JWT и други произволни символи е стандартизацията на съдържанието на маркера. Друг препоръчителен подход е изпращането на JWT токена в Authorization
заглавка, използвайки схемата Bearer. Съдържанието на заглавката трябва да изглежда така:
Authorization: Bearer
За да работят REST услугите според очакванията, се нуждаем от малко по-различен подход за оторизация в сравнение с класическите уебсайтове с много страници.
python машинно обучение чрез пример
Вместо да задейства процеса на удостоверяване чрез пренасочване към страница за вход, когато клиентът поиска защитен ресурс, REST сървърът удостоверява всички заявки, използвайки данните, налични в самата заявка, JWT токен в този случай. Ако такова удостоверяване се провали, пренасочването няма смисъл. REST API просто изпраща отговор на HTTP код 401 (неоторизиран) и клиентите трябва да знаят какво да правят; например, браузърът ще покаже динамичен div, за да позволи на потребителя да предостави потребителското име и паролата.
От друга страна, след успешно удостоверяване в класически, многостранични уебсайтове, потребителят се пренасочва с помощта на HTTP код 301 (Премества се постоянно), обикновено към начална страница или, още по-добре, към страницата, която потребителят първоначално е поискал, която е задействала процеса на удостоверяване. С REST отново това няма смисъл. Вместо това просто ще продължим с изпълнението на заявката, сякаш ресурсът изобщо не е защитен, връщаме HTTP код 200 (ОК) и очакваното тяло на отговор.
Сега, нека видим как можем да внедрим REST API, базиран на токен JWT, използвайки Java и Пролет , докато се опитваме да използваме поведението по подразбиране на Spring Security, където можем.
Както се очаква, Spring Security framework се предлага с много готови за добавяне класове, които се занимават със „стари“ механизми за оторизация: бисквитки на сесията, HTTP Basic и HTTP Digest. Липсва обаче родната поддръжка за JWT и трябва да си замърсим ръцете, за да работи. За по-подробен преглед трябва да се консултирате с официален представител Документация за Spring Security .
Сега да започнем с обичайното Дефиниция на филтъра Spring Security в web.xml
:
springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /*
Имайте предвид, че името на филтъра Spring Security трябва да бъде точно springSecurityFilterChain
за останалата част от Spring конфигурацията да работи от кутията.
Следва XML декларацията на Spring фасул, свързана със сигурността. За да опростим XML, ще зададем пространството от имена по подразбиране на security
чрез добавяне на xmlns='http://www.springframework.org/schema/security'
към основния XML елемент. Останалата част от XML изглежда така:
гещалт принцип за добро продължение
(1) (2) (3) (4) (5) (6) (7) (8)
@PreFilter
, @PreAuthorize
, @PostFilter
, @PostAuthorize
анотации за всеки пролетен боб в контекста.stateless
(не искаме сесията, създадена с цел сигурност, тъй като използваме маркери за всяка заявка).csrf
защита, защото нашите символи са имунизирани срещу нея.AbstractAuthenticationProcessingFilter
, трябва да го декларираме в XML, за да свържем свойствата му (автоматичното свързване тук не работи). По-късно ще обясним какво прави филтърът.AbstractAuthenticationProcessingFilter
не е достатъчно добър за REST цели, защото пренасочва потребителя към страница за успех; Ето защо ние поставяме нашите собствени тук.authenticationManager
се използва от нашия филтър за удостоверяване на потребителите.Сега нека видим как реализираме конкретните класове, декларирани в XML по-горе. Имайте предвид, че Spring ще ни ги свърже. Започваме с най-простите.
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // This is invoked when user tries to access a secured REST resource without supplying any credentials // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 'Unauthorized'); } }
Както е обяснено по-горе, този клас просто връща HTTP код 401 (Неразрешен), когато удостоверяването е неуспешно, заменяйки пренасочването по подразбиране на Spring.
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // We do not need to do anything extra on REST authentication success, because there is no page to redirect to } }
Това просто заменяне премахва поведението по подразбиране на успешно удостоверяване (пренасочване към начална или друга страница, която потребителят поиска). Ако се чудите защо не е необходимо да заменим AuthenticationFailureHandler
, това е така, защото изпълнението по подразбиране няма да пренасочва никъде, ако неговият URL адрес за пренасочване не е зададен, така че просто избягваме да задаваме URL адреса, който е достатъчно добър.
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public JwtAuthenticationFilter() { super('/**'); } @Override protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return true; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String header = request.getHeader('Authorization'); if (header == null || !header.startsWith('Bearer ')) { throw new JwtTokenMissingException('No JWT token found in request headers'); } String authToken = header.substring(7); JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken); return getAuthenticationManager().authenticate(authRequest); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { super.successfulAuthentication(request, response, chain, authResult); // As this authentication is in HTTP header, after success we need to continue the request normally // and return the response as if the resource was not secured at all chain.doFilter(request, response); } }
Този клас е входната точка на нашия процес за удостоверяване на JWT; филтърът извлича маркера JWT от заглавките на заявката и делегира удостоверяване на инжектирания AuthenticationManager
. Ако маркерът не бъде намерен, се хвърля изключение, което спира обработката на заявката. Също така се нуждаем от заместване за успешно удостоверяване, защото стандартният поток Spring ще спре веригата на филтъра и ще продължи с пренасочване. Имайте предвид, че имаме нужда веригата да се изпълни изцяло, включително да генерира отговора, както е обяснено по-горе.
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private JwtUtil jwtUtil; @Override public boolean supports(Class authentication) { return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication; String token = jwtAuthenticationToken.getToken(); User parsedUser = jwtUtil.parseToken(token); if (parsedUser == null) { throw new JwtTokenMalformedException('JWT token is not valid'); } List authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(parsedUser.getRole()); return new AuthenticatedUser(parsedUser.getId(), parsedUser.getUsername(), token, authorityList); } }
В този клас използваме Spring's default AuthenticationManager
, но го инжектираме със собствените си AuthenticationProvider
което прави действителния процес на удостоверяване. За да приложим това, ние разширяваме AbstractUserDetailsAuthenticationProvider
, което изисква само да върнем UserDetails
въз основа на заявката за удостоверяване, в нашия случай, JWT токенът, обвит в JwtAuthenticationToken
клас. Ако токенът не е валиден, ние хвърляме изключение. Ако обаче е валидно и дешифриране от JwtUtil
е успешен, извличаме потребителски данни (ще видим как точно в класа JwtUtil
), без изобщо да имаме достъп до базата данни. Цялата информация за потребителя, включително неговите или нейните роли, се съдържа в самия маркер.
public class JwtUtil { @Value('${jwt.secret}') private String secret; /** * Tries to parse specified String as a JWT token. If successful, returns User object with username, id and role prefilled (extracted from token). * If unsuccessful (token is invalid or not containing all required user properties), simply returns null. * * @param token the JWT token to parse * @return the User object extracted from specified token or null if a token is invalid. */ public User parseToken(String token) { try { Claims body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); User u = new User(); u.setUsername(body.getSubject()); u.setId(Long.parseLong((String) body.get('userId'))); u.setRole((String) body.get('role')); return u; } catch (JwtException | ClassCastException e) { return null; } } /** * Generates a JWT token containing username as subject, and userId and role as additional claims. These properties are taken from the specified * User object. Tokens validity is infinite. * * @param u the user for which the token will be generated * @return the JWT token */ public String generateToken(User u) { Claims claims = Jwts.claims().setSubject(u.getUsername()); claims.put('userId', u.getId() + ''); claims.put('role', u.getRole()); return Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } }
И накрая, JwtUtil
class е отговорен за синтактичния анализ на маркера на User
обект и генериране на маркера от User
обект. Това е лесно, тъй като използва jjwt
библиотека да свърши цялата JWT работа. В нашия пример ние просто съхраняваме потребителското име, потребителския идентификатор и потребителските роли в маркера. Също така бихме могли да съхраняваме повече произволни неща и да добавяме повече функции за сигурност, като изтичането на маркера. Синтактичният анализ на маркера се използва в AuthenticationProvider
както е показано по-горе. generateToken()
метод се извиква от услугите REST за вход и регистрация, които са незащитени и няма да задействат проверки за сигурност или да изискват маркер да присъства в заявката. В крайна сметка той генерира маркера, който ще бъде върнат на клиентите въз основа на потребителя.
Въпреки че старите стандартизирани подходи за защита (бисквитка на сесията, HTTP Basic и HTTP Digest) ще работят и с REST услуги, всички те имат проблеми, които би било хубаво да се избегнат, като се използва по-добър стандарт. JWT пристига точно навреме, за да спаси деня и най-важното е много близо до превръщането си в IETF стандарт.
Основната сила на JWT е обработката на удостоверяването на потребителя по бездържавен и следователно мащабируем начин, като същевременно запазва всичко сигурно с актуални криптографски стандарти. Съхраняването на искания (потребителски роли и разрешения) в самия маркер създава огромни предимства в разпределените системни архитектури, където сървърът, който издава заявката, няма достъп до източника на данни за удостоверяване.
REST, съкратено от Reprezentative State Transfer, е архитектурен стил за излагане на последователни приложни програмни интерфейси (API) между уеб услугите.
JSON Web Token (JWT) е стандарт за кодиране на информация, която може да бъде сигурно предадена като JSON обект.