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использовать в окружающей среде. Процесс сертификации выглядит следующим образом:
Инициировано клиентом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获取更多资讯