Spring Security борется с галантерейными товарами: используйте JWT-аутентификацию для доступа к интерфейсу

Spring Boot

jwt.png

1. Введение

добро пожаловать на чтениеПрактичная серия галантереи Spring Security. Ранее я объяснял, как написать свой собственныйJwtГенератор и как вернуться после аутентификации пользователяJson Web Token. Сегодня посмотрим, как использовать в запросахJwtАутентификация доступа.DEMOКак его получить — в конце статьи.

2. Общие методы HTTP-аутентификации

мы вHttpИспользовать в запросеJwtмы должны понять общееHttpметод проверки.

2.1 HTTP Basic Authentication

HTTP Basic AuthenticationТакже называемая базовой аутентификацией, она просто используетBase64Алгоритм шифрует имя пользователя и пароль и помещает зашифрованную информацию в заголовок запроса.HeaderПо сути, имя пользователя и пароль передаются открытым текстом, что небезопасно, поэтому лучшеHttpsиспользовать в окружающей среде. Процесс сертификации выглядит следующим образом:

basic.png

Инициировано клиентомGETзапросить ответ сервера401 Unauthorized,www-Authenticateуказать алгоритм аутентификации,realmУказывает домен безопасности. Затем клиент обычно выдает всплывающее окно с запросом имени пользователя и пароля, введите имя пользователя и пароль и поместите его вHeaderЗапросите еще раз, после успешной аутентификации сервера200Код состояния отвечает клиенту.

2.2 HTTP Digest Authentication

Чтобы компенсировать недостатки сертификации BASIC,HTTP Digest Authentication. Это также называется дайджест-аутентификацией. Он использует случайные числа плюсMD5Алгоритм переваривания имени пользователя и пароля, процесс аналогиченHttp Basic Authentication, но немного сложнее:

Шаг 1. То же, что и обычная проверка подлинности, но возврат сWWW-AuthenticateОтвет с полями заголовка. Это поле содержит временный код консультации (случайное число,nonce). поле заголовкаWWW-Authenticateдолжен содержатьrealmиnonceинформацию об этих двух полях. Клиент аутентифицируется, отправляя эти два значения обратно на сервер.nonceэто своего рода возвращение каждый раз401Ответ на сгенерированную произвольную случайную строку. Эта строка обычно рекомендуетсяBase64Состав закодированного шестнадцатеричного числа, но фактическое содержание зависит от конкретной реализации сервера

Шаг 2: Получите401Клиент кода состояния, возвращенный ответ содержит обязательные поля заголовка для проверки подлинности DIGEST.AuthorizationИнформация. поле заголовкаAuthorizationдолжен содержатьusername、realm、nonce、uriиresponseинформация о поле, где,realmиnonceполя в ответе, ранее полученном от сервера.

Шаг 3: Сервер, получивший запрос на авторизацию, содержащий поле заголовка, подтвердит правильность данных аутентификации. После прохождения аутентификации он вернется, содержащийRequest-URIОтвет ресурса.

И на этот раз будет в поле заголовкаAuthorization-InfoНапишите некоторую информацию об успешной аутентификации.

2.3 Аутентификация клиента SSL

SSLАутентификация клиента — это то, что мы обычно называемHTTPS. Уровень безопасности высокий, но необходимо предпринятьCAПлата за сертификат.SSLНекоторые важные концепции участвуют в процессе аутентификации, такими как открытый ключ органа цифрового сертификата, закрытый ключ и открытый ключ сертификата, асимметричный алгоритм (используется с частным ключом и открытым ключом сертификата), симметричный ключ, и симметричный алгоритм (с симметричным шифрованием). Используемый ключ). Это относительно сложно, но я не буду говорить об этом здесь.

2.4 Сертификация формы формы

Форма Метод проверки подлинности формы не является спецификацией HTTP. Таким образом, методы реализации также разнообразны.На самом деле, наш обычный вход в систему с помощью скан-кода и входа с кодом подтверждения мобильного телефона относится к категории входа в форму. Аутентификация формы обычно взаимодействуетCookie,Sessionиспользования, теперь многиеWebСайты используют этот метод аутентификации. Пользователь вводит имя пользователя и пароль на странице входа, после прохождения аутентификации на сервереsessionIdВернитесь в браузер, браузер сохранитsessionIdв браузерCookieсередина.因为 HTTP 是无状态的,所以浏览器使用CookieсохранитьsessionId. В следующий раз клиент отправит запрос сsessionIdзначение, найденное серверомsessionIdСуществуйте и используйте его в качестве индекса для получения информации об аутентификации сервера присутствия пользователей для выполнения операций аутентификации. Аутентификация обеспечит доступ к ресурсам.

мы вФактические боевые галантерейные товары Spring Security: верните токен JWT после входа в системуНа самом деле, это также черезFormОтправить, чтобы получитьJwtфактическиJwtиsessionIdтот же эффект, ноJwtОн, естественно, несет некоторую информацию о пользователе, иsessionIdНужно пойти дальше, чтобы получить информацию о пользователе.

2.5 JSON Web Token Authentication Аутентификация

Мы получаем через форму сертификацииJson Web Token, так как его использовать?Обычно мы ставимJwtИспользовать как токенBearer Authenticationспособ использования.Bearer Authenticationоснован на токенахHTTPВ схеме аутентификации, когда пользователи запрашивают доступ к ограниченным ресурсам с сервера, они будут иметь маркер в качестве учетных данных, и если они пройдут проверку, они смогут получить доступ к определенным ресурсам. изначально был вRFC 6750в качествеOAuth 2.0часть , но иногда может использоваться отдельно. мы используемBear TokenМетод находится в заголовке запросаAuthorizationположить в полеBearer <token>зашифрованная строка в формате (Json Web Token).Обратите внимание, что между префиксом носителя и токеном есть нулевой символ.Подобно базовой аутентификации, аутентификация носителя может использоваться только через HTTPS (SSL).

3. Реализовать Jwt-аутентификацию интерфейса в Spring Security.

Далее мы изюминка нашей серии - интерфейсJwtСертификация.

3.1 Определение фильтра веб-токенов Json

Независимо от того, какой метод аутентификации упомянут выше, мы можем использоватьSpring SecurityсерединаFilterобрабатывать. Базовая конфигурация Spring Security по умолчанию не обеспечиваетBearer AuthenticationОбрабатывать фильтр, но обеспечивает обработкуBasic Authenticationфильтр:

org.springframework.security.web.authentication.www.BasicAuthenticationFilter

BasicAuthenticationFilterнаследоватьOncePerRequestFilter. Так что мы также подражаемBasicAuthenticationFilterреализовать свое собственноеJwtAuthenticationFilter. Полный код выглядит следующим образом:

 package cn.felord.spring.security.filter;
 
 import cn.felord.spring.security.exception.SimpleAuthenticationEntryPoint;
 import cn.felord.spring.security.jwt.JwtTokenGenerator;
 import cn.felord.spring.security.jwt.JwtTokenPair;
 import cn.felord.spring.security.jwt.JwtTokenStorage;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpHeaders;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.List;
 import java.util.Objects;
 
 /**
  * jwt 认证拦截器 用于拦截 请求 提取jwt 认证
  *
  * @author dax
  * @since 2019/11/7 23:02
  */
 @Slf4j
 public class JwtAuthenticationFilter extends OncePerRequestFilter {
     private static final String AUTHENTICATION_PREFIX = "Bearer ";
     /**
      * 认证如果失败由该端点进行响应
      */
     private AuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
     private JwtTokenGenerator jwtTokenGenerator;
     private JwtTokenStorage jwtTokenStorage;
 
 
     public JwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
         this.jwtTokenGenerator = jwtTokenGenerator;
         this.jwtTokenStorage = jwtTokenStorage;
     }
 
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
         // 如果已经通过认证
         if (SecurityContextHolder.getContext().getAuthentication() != null) {
             chain.doFilter(request, response);
             return;
         }
         // 获取 header 解析出 jwt 并进行认证 无token 直接进入下一个过滤器  因为  SecurityContext 的缘故 如果无权限并不会放行
         String header = request.getHeader(HttpHeaders.AUTHORIZATION);
         if (StringUtils.hasText(header) && header.startsWith(AUTHENTICATION_PREFIX)) {
             String jwtToken = header.replace(AUTHENTICATION_PREFIX, "");
 
 
             if (StringUtils.hasText(jwtToken)) {
                 try {
                     authenticationTokenHandle(jwtToken, request);
                 } catch (AuthenticationException e) {
                     authenticationEntryPoint.commence(request, response, e);
                 }
             } else {
                 // 带安全头 没有带token
                 authenticationEntryPoint.commence(request, response, new AuthenticationCredentialsNotFoundException("token is not found"));
             }
 
         }
         chain.doFilter(request, response);
     }
 
     /**
      * 具体的认证方法  匿名访问不要携带token
      * 有些逻辑自己补充 这里只做基本功能的实现
      *
      * @param jwtToken jwt token
      * @param request  request
      */
     private void authenticationTokenHandle(String jwtToken, HttpServletRequest request) throws AuthenticationException {
 
         // 根据我的实现 有效token才会被解析出来
         JSONObject jsonObject = jwtTokenGenerator.decodeAndVerify(jwtToken);
 
         if (Objects.nonNull(jsonObject)) {
             String username = jsonObject.getStr("aud");
 
             // 从缓存获取 token
             JwtTokenPair jwtTokenPair = jwtTokenStorage.get(username);
             if (Objects.isNull(jwtTokenPair)) {
                 if (log.isDebugEnabled()) {
                     log.debug("token : {}  is  not in cache", jwtToken);
                 }
                 // 缓存中不存在就算 失败了
                 throw new CredentialsExpiredException("token is not in cache");
             }
             String accessToken = jwtTokenPair.getAccessToken();
 
             if (jwtToken.equals(accessToken)) {
                   // 解析 权限集合  这里
                 JSONArray jsonArray = jsonObject.getJSONArray("roles");
 
                 String roles = jsonArray.toString();
 
                 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
                 User user = new User(username, "[PROTECTED]", authorities);
                 // 构建用户认证token
                 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
                 usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                 // 放入安全上下文中
                 SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
             } else {
                 // token 不匹配
                 if (log.isDebugEnabled()){
                     log.debug("token : {}  is  not in matched", jwtToken);
                 }
 
                 throw new BadCredentialsException("token is not matched");
             }
         } else {
             if (log.isDebugEnabled()) {
                 log.debug("token : {}  is  invalid", jwtToken);
             }
             throw new BadCredentialsException("token is invalid");
         }
     }
 }

Подробности смотрите в разделе комментариев к коду.Некоторые части логики корректируются в соответствии с вашим бизнесом.Анонимный доступ не должен приносить Token!

3.2 Настройка JwtAuthenticationFilter

сначала отфильтроватьJwtAuthenticationFilterинъекцияSpring IoCконтейнер, то обязательно поставьтеJwtAuthenticationFilterрасставлены по порядкуUsernamePasswordAuthenticationFilterДо:

        @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
                     .cors()
                     .and()
                     // session 生成策略用无状态策略
                     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                     .and()
                     .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                     .and()
                     .authorizeRequests().anyRequest().authenticated()
                     .and()
                     .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
                     // jwt 必须配置于 UsernamePasswordAuthenticationFilter 之前
                     .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                     // 登录  成功后返回jwt token  失败后返回 错误信息
                     .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
                     .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
 
         }

4. Запросите проверку с помощью Jwt

Напишите ограниченный интерфейс, мы здесьhttp://localhost:8080/foo/test. Прямые запросы будут401. Получаем его по следующему рисункуToken :

затем вPostmanиспользуется вJwt :

В конце концов, аутентификация пройдет успешно, и доступ к ресурсу будет осуществлен.

5. Обновить токен JWT

мы вРеальные боевые галантерейные товары Spring Security: научим вас внедрять JWT Tokenбыл реализован вJson Web TokenЭто вся логика в парах.accessTokenиспользуется для запросов интерфейса,refreshTokenобновитьaccessToken. Мы также можем определитьFilterСм. вышеJwtAuthenticationFilter. Просто этот запрос несетrefreshToken, перехватываем в фильтреURIСоответствует конечной точке обновления, которую мы определили. Также проверьтеTokenи вернуть пару токенов, как если бы вход был успешным после прохождения. Здесь не будет демонстрации кода.

6. Резюме

Это серия оригинальных статей, всегда найдутся студенты, которые невнимательно читают, не могут ухватиться за ум и критиковать их. Еду надо есть по кусочку, а готовой еды нет, тут все кончено, куда торопиться. Оригинальность не так проста, внимание является движущей силой.Практичная серия галантереи Spring SecurityКаждая статья имеет разные точки знаний, и все они связаны друг с другом. Если вы не понимаете, вы оглянетесь назад. Spring Security не сложно учиться, ключ заключается в том, что вы не думаете о том, как думать. в этот разDEMOВы можете подписаться на публичный аккаунт:FelordcnОтветитьss08Получать.

关注公众号:Felordcn获取更多资讯

Личный блог: https://felord.cn