На бумаге в конце концов я чувствую себя мелким, и я понимаю, что в этом вопросе нужно практиковаться.
клин
Эта статья подходит для:Студенты, которые немного разбираются в Spring Security или запустили простую демонстрацию, но не понимают общего процесса работы, те, кто интересуется Spring Security, также могут использовать его в качестве вводного руководства.В примере кода также есть много комментариев. .
Код для этой статьи: Адрес облака кода Адрес GitHub
Когда все создают систему, первый модуль, который обычно делается, этоАутентификация и авторизацияМодуль, потому что это вход в систему, а также самая важная и основная часть системы.После того, как служба аутентификации и авторизации спроектирована и построена, к остальным модулям можно получить безопасный доступ.
Общая структура аутентификации и авторизации на рынке такова:shiro
иSpring Security
, и большинство компаний предпочитают разрабатывать свои собственные. Я видел много раньшеSpring Security
Вводный урок , но я не думаю, что он очень хорош, поэтому последние два дня я возился с ним.Spring Security
Когда мне пришла в голову идея поделиться, я надеюсь, что это может помочь тем, кто заинтересован.
Spring Security
Фреймворк, который мы в основном используем, предназначен для решения функции аутентификации и авторизации, поэтому моя статья будет разделена на две части:
- Первая часть сертификации (данная статья)
- Вторая часть авторизации (в следующей статье)
Я буду использовать демонстрацию Spring Security + JWT + cache, чтобы показать, о чем я хочу рассказать, ведь вещи в мозгу должны отражаться в конкретных вещах, чтобы каждый мог понимать и понимать более интуитивно.
При изучении нового я рекомендую использовать метод обучения сверху вниз, чтобы вы могли лучше понять новое, а не слепо касаться слона.
Примечание: он включает только аутентификацию и авторизацию пользователя и не требует авторизации третьей стороны, такой как oauth2.
1. 📖 Рабочий процесс Spring Security
Если вы хотите начать работу со Spring Security, вы должны сначала понять его рабочий процесс, потому что он не похож на набор инструментов, он готов к использованию, вы должны иметь определенное представление о нем, а затем настроить его в соответствии с его использованием.
Сначала мы можем взглянуть на его рабочий процесс:
существуетSpring Security的
В официальном документе есть такое предложение:
Веб-инфраструктура Spring Security полностью основана на стандартных фильтрах сервлетов.
Веб-основой Spring Security являются фильтры.
Это предложение показываетSpring Security
Дизайнерское мышление:То есть веб-запросы обрабатываются через уровни фильтров.
положить реальныйSpring Security
, на словах можно сказать так:
Веб-запрос будет проходить через цепочку фильтров, а аутентификация и авторизация будут завершены в процессе прохождения через цепочку фильтров. API, а затем будет выдано исключение. Обработчики исключений обрабатывают эти исключения.
Если вы используете картинку для описания, вы можете нарисовать ее так, как я нашел на Baidu:
Как показано на рисунке выше, запрос, который хочет получить доступ к API, будет проходить через фильтры в синей проволочной рамке слева направо.Зеленая часть — это фильтр, отвечающий за аутентификацию, о которой мы в основном говорим в этой статье, а синяя часть отвечает за обработку исключений, оранжевая часть отвечает за авторизацию.
Сегодня мы не будем говорить о двух зеленых фильтрах на картинке, потому что это два встроенных фильтра Spring Security для проверки подлинности с помощью формы и обычной проверки подлинности, а наша демонстрация — это проверка подлинности JWT, поэтому она не используется.
если вы использовалиSpring Security
Вы должны знать, что в конфигурации есть два, называемыхformLogin
иhttpBasic
Элементы конфигурации, откройте их в конфигурации, соответствующей открытию вышеуказанного фильтра.
-
formLogin
Соответствует вашему методу проверки подлинности формы, а именно UsernamePasswordAuthenticationFilter. -
httpBasic
Соответствует методу базовой аутентификации, то есть BasicAuthenticationFilter.
Другими словами, если вы настроите эти два метода аутентификации, они будут добавлены в цепочку фильтров, иначе они не будут добавлены в цепочку фильтров.
так какSpring Security
Во встроенном фильтре нет метода аутентификации для JWT, поэтому наша демонстрацияНапишите фильтр аутентификации JWT и поставьте его в зеленую позицию для аутентификации.
2. 📝Важные концепции SpringSecurity
Зная общий рабочий процесс Spring Security, нам также необходимо знать некоторые очень важные концепции, которые также можно назвать компонентами:
-
SecurityContext: объект контекста,
Authentication
Объекты будут размещены внутри. - SecurityContextHolder: статический служебный класс для получения объекта контекста.
- Authentication: Интерфейс аутентификации, определяющий форму данных объекта аутентификации.
-
AuthenticationManager: для подтверждения
Authentication
, возвращает завершенную аутентификациюAuthentication
объект.
1.SecurityContext
Объект контекста, в котором размещаются аутентифицированные данные, интерфейс определяется следующим образом:
public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}
В этом интерфейсе всего два метода, основная функция которых — получить или установитьAuthentication
.
2. SecurityContextHolder
public class SecurityContextHolder {
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
ты мог бы так сказатьSecurityContext
Класс инструмента для получения, установки или очисткиSecurityContext
, по умолчанию все данные будут храниться в текущем потоке.
3. Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Эффекты этих методов следующие:
-
getAuthorities
: получить права пользователя, как правило,Информация о роли пользователя. -
getCredentials
: получение информации, подтверждающей аутентификацию пользователя, обычно это пароль и другая информация. -
getDetails
: Получить дополнительную информацию о пользователе (эта часть информации может быть информацией в нашей пользовательской таблице). -
getPrincipal
: получить информацию об удостоверении пользователя, в случае отсутствия аутентификации имя пользователя получается,В случае аутентификации получаются UserDetails. -
isAuthenticated
: получить текущийAuthentication
Сертифицирован ли он. -
setAuthenticated
: установить текущийAuthentication
Является ли оно аутентифицированным (истинным или ложным).
Authentication
Он просто определяет, какой должна быть форма данных, аутентифицированных в Spring Security.Он должен иметь разрешения, пароли, идентификационную информацию и дополнительную информацию.
4. AuthenticationManager
public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager
Определяет метод аутентификации, который принимаетAuthentication
передано, возвращает аутентифицированныйAuthentication
, класс реализации по умолчанию: ProviderManager.
Далее вы можете подумать о том, как последовательно соединить эти четыре части, чтобы сформировать процесс аутентификации Spring Security:
1. 👉Сначала приходит запрос с идентификационной информацией
2. 👉ПослеAuthenticationManager
сертификация,
3. 👉Пройти еще разSecurityContextHolder
ПолучатьSecurityContext
,
4. 👉Наконец внесите заверенную информацию вSecurityContext
.
3. 📃 Подготовка перед кодом
Прежде чем мы действительно начнем рассказывать наш код аутентификации, нам сначала нужно импортировать необходимые зависимости.Зависимости, связанные с базой данных, могут выбрать, какой JDBC framework.Я использую вторичную разработку китайцев здесь.myabtis-plus.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
Далее нам нужно определить несколько необходимых компонентов.
Поскольку я использую Spring-Boot версии 2.X, мы должны сами определить шифровальщик:
1. Определите компонент шифрования
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Эта фасоль необходима,Spring Security
Определенное нами шифрование будет использоваться во время операции аутентификации, в противном случае возникнет исключение.
2. Определите AuthenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
здесь будетSpring Security
автономныйauthenticationManager
Объявленный как Bean, функция его объявления состоит в том, чтобы использовать его, чтобы помочь нам выполнить операции аутентификации, и вызвать функцию Bean.authenticate
метод будетSpring Security
Автоматически помогите нам выполнить аутентификацию.
3. Внедрить службу сведений о пользователях
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}",s);
// 根据用户名验证用户
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
UserInfo userInfo = userService.getOne(queryWrapper);
if (userInfo == null) {
throw new UsernameNotFoundException("用户名不存在,登陆失败。");
}
// 构建UserDetail对象
UserDetail userDetail = new UserDetail();
userDetail.setUserInfo(userInfo);
List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
userDetail.setRoleInfoList(roleInfoList);
return userDetail;
}
}
выполнитьUserDetailsService
абстрактный метод, который возвращаетUserDetailsObject, Spring Security вызовет этот метод для доступа к базе данных для поиска пользователей в процессе аутентификации Логика может быть настроена, будь то из базы данных или из кеша, но нам нужно собрать информацию о пользователе и информацию о разрешениях, которые мы запросили, в ОдинUserDetailsвозвращение.
UserDetailsЭто также интерфейс, который определяет форму данных, которые используются для сохранения данных, которые мы находим в базе данных. Его функции в основном заключаются в проверке состояния учетной записи и получении разрешений. Для конкретной реализации вы можете обратиться к моему складу.код.
4. TokenUtil
Поскольку мы находимся в режиме аутентификации JWT, нам также нужен класс инструмента, который помогает нам работать с Token, Вообще говоря, достаточно иметь следующие три метода:
- создать токен
- проверить токен
- Обратный анализ информации в токене
В моем коде ниже JwtProvider действует как класс инструмента Token, и конкретную реализацию можно найти на моем складе.код.
4. ✍ Конкретная реализация в коде
После предыдущего объяснения каждый должен знать, как использоватьSpringSecurity
Чтобы выполнить аутентификацию JWT, нам нужно написать фильтр для проверки JWT, а затем поместить этот фильтр в зеленую часть.
Прежде чем мы напишем этот фильтр, нам также необходимо выполнить операцию аутентификации, потому что нам нужно получить доступ к интерфейсу аутентификации, чтобы получить токен, прежде чем мы сможем поместить токен в заголовок запроса и сделать следующий запрос.
Если вы этого не понимаете, не волнуйтесь, просто читайте дальше, и я снова разберусь с этим в конце этого раздела.
1. Метод аутентификации
При доступе к системе метод аутентификации обычно является первым, к чему нужно получить доступ.Здесь я написал самые простые шаги, необходимые для аутентификации, потому что в реальной системе нам также нужно писать записи входа в систему, расшифровывать пароль на переднем плане, и эти операции.
@Override
public ApiResult login(String loginAccount, String password) {
// 1 创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
// 2 认证
Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
// 3 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4 生成自定义token
UserDetail userDetail = (UserDetail) authentication.getPrincipal();
AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());
// 5 放入缓存
caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
return ApiResult.ok(accessToken);
}
Всего здесь пять шагов, вероятно, только первые четыре относительно незнакомы:
- Передача имени пользователя и пароля создает
UsernamePasswordAuthenticationToken
объект, это то, что мы сказали ранееAuthentication
Класс реализации, передайте имя пользователя и пароль в качестве параметров построения, этот объект является неаутентифицированным объектом, который мы создали.Authentication
объект. - Используя Bean, который мы объявили ранее -
authenticationManager
назови этоauthenticate
метод аутентификации и возврата аутентифицированногоAuthentication
объект. - Если после завершения аутентификации нет никаких отклонений, он перейдет к третьему шагу, используя
SecurityContextHolder
ПолучатьSecurityContext
После этого, после завершения аутентификации,Authentication
объект, в объект контекста. - от
Authentication
объект, чтобы получить нашUserDetails
Объект, как мы уже говорили, аутентифицированныйAuthentication
объект вызывает свойgetPrincipal()
Метод может получить собранный результат после нашего предыдущего запроса к базе данных.UserDetails
объект, а затем создать токен. - Пучок
UserDetails
Объект помещается в кеш, что удобно для последующего использования фильтров.
В этом случае, даже если он завершен, он кажется очень простым, потому что основные операции аутентификации будут выполнятьсяauthenticationManager.authenticate()
Помогите нам закончить.
Далее мы можем взглянуть на исходный код и увидеть, как Spring Security помогает нам выполнить эту аутентификацию (часть опущена):
// AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication){
// 校验未认证的Authentication对象里面有没有用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中去查用户名为XXX的对象
UserDetails user = this.userCache.getUserFromCache(username);
// 如果没有就进入到这个方法
if (user == null) {
cacheWasUsed = false;
try {
// 调用我们重写UserDetailsService的loadUserByUsername方法
// 拿到我们自己组装好的UserDetails对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 校验账号是否禁用
preAuthenticationChecks.check(user);
// 校验数据库查出来的密码,和我们传入的密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
}
Прочитав исходный код, вы обнаружите, что, как мы обычно пишем, основная логика заключается в проверке базы данных и последующем сравнении пароля.
После входа эффект следующий:
После того, как мы вернем токен, в следующий раз, когда мы будем запрашивать другие API, нам нужно привести этот токен в заголовок запроса, что можно сделать по стандарту JWT.
2. JWT-фильтр
После того, как у нас есть токен, нам нужно поместить фильтр в цепочку фильтров для анализа токена. Поскольку у нас нет сеанса, каждый раз, когда мы определяем, какой запрос пользователя, он основан на токене в запросе. вне который пользователь в настоящее время.
Итак, нам нужен фильтр для перехвата всех запросов, как мы уже говорили ранее, мы поместим этот фильтр в зеленую часть, чтобы заменить его.UsernamePasswordAuthenticationFilter
, поэтому мы создаем новыйJwtAuthenticationTokenFilter
, а затем зарегистрируйте его как Bean, и вам нужно добавить это при написании файла конфигурации:
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter(),
UsernamePasswordAuthenticationFilter.class);
}
addFilterBefore
Семантика заключается в том, чтобы добавить фильтр к XXXFilter, поместите его здесь, чтобы поместитьJwtAuthenticationTokenFilter
ставитьUsernamePasswordAuthenticationFilter
Ранее, поскольку выполнение фильтров также является последовательным, мы должны поместить наш фильтр в зеленую часть цепочки фильтров, чтобы добиться эффекта автоматической аутентификации.
Далее мы можем увидетьJwtAuthenticationTokenFilter
Конкретная реализация:
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain) throws ServletException, IOException {
log.info("JWT过滤器通过校验请求头token进行自动登录...");
// 拿到Authorization请求头内的信息
String authToken = jwtProvider.getToken(request);
// 判断一下内容是否为空且是否为(Bearer )开头
if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
// 去掉token前缀(Bearer ),拿到真实token
authToken = authToken.substring(jwtProperties.getTokenPrefix().length());
// 拿到token里面的登录账号
String loginAccount = jwtProvider.getSubjectFromToken(authToken);
if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 缓存里查询用户,不存在需要重新登陆。
UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);
// 拿到用户信息后验证用户信息与token
if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {
// 组装authentication对象,构造参数是Principal Credentials 与 Authorities
// 后面的拦截器里面会用到 grantedAuthorities 方法
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
}
}
}
chain.doFilter(request, response);
}
Хотя шаги в коде очень подробные, его может быть трудно читать, потому что код слишком длинный, я просто расскажу об этом, или вы можете пойти на склад, чтобы проверитьисходный код:
- получить
Authorization
Информация о токене, соответствующая заголовку запроса - Удалить заголовок токена (Bearer)
- Разбираем токен и получаем учетную запись, которую мы в него вставили
- Так как мы уже залогинились раньше, то получаем прямо из кеша
UserDetail
Информация - Проверьте, имеет ли UserDetail значение null, и проверьте, не истек ли срок действия токена,
UserDetail
Совпадают ли имя пользователя и токен. - собрать один
authentication
объект, поместите его в объект контекста, чтобы последний фильтр увидел, что у нас есть объект контекстаauthentication
Объект эквивалентен тому, который мы уже аутентифицировали.
Таким образом, после каждого запроса с правильным токеном информация о его учетной записи будет найдена и помещена в объект контекста, мы можем использоватьSecurityContextHolder
Очень удобно получать объект контекстаAuthentication
объект.
После завершения запустите нашу демонстрацию, вы можете увидеть, что в цепочке фильтров есть следующие фильтры, из которых мы настроили пятый:
🐱🏍Просто подливаем информацию об учетной записи и роли полученные после входа в кеш.Когда придет запрос с токеном, мы вытащим его из кеша и снова положим в объект контекста. .
В сочетании с методом аутентификации наша логическая цепочка становится:
Авторизоваться 👉Получить токен 👉Запросить принести токен 👉JWT фильтр для перехвата 👉Проверить токен 👉Найденный из кеша объект помещен в контекст
После этого наша логика аутентификации завершена.
4. 💡Оптимизация кода
После того, как проверка подлинности и JWT-фильтр завершены, проект JWT может действительно запускаться и достигать желаемого эффекта.Если мы хотим сделать программу более надежной, нам нужно добавить некоторые вспомогательные функции, чтобы сделать код более дружественным.
1. Обработчик ошибок аутентификации
Этот обработчик запускается, когда пользователь не вошел в систему или синтаксический анализ токена завершается с ошибкой, возвращая недопустимый результат доступа.
2. Недостаточно прав для процессора
Когда собственные разрешения пользователя не соответствуют разрешениям, требуемым API, к которому осуществляется доступ, срабатывает этот обработчик, и возвращается результат недостаточности разрешений.
3. Метод выхода
Выход пользователя обычно осуществляется путем очистки объекта контекста и кеша.Также можно выполнить дополнительные операции.Эти два шага необходимы.
4. Обновление токена
Обновление токена проекта JWT также важно.Здесь основной метод обновления токена помещается в класс инструмента токена.После обновления кеш можно перезагрузить снова, потому что кеш действителен, а повторная установка может сбросить срок действия время.
постскриптум
Я думаю об этой статье с прошлого воскресенья.Чтобы старушка ее поняла,мне приходится ее несколько раз пересматривать, прежде чем опубликовать.
Spring Security
Это действительно немного сложно начать. Когда я впервые узнал об этом, я посмотрел учебник Shang Silicon Valley. Лектор видео объединил его с Thymeleaf, что привело к тому, что многие блоги в Интернете стали говорить об этом.Spring Security
Точно так же, когда приходит время, не обращая внимания на разделение переднего и заднего концов.
Существуют также учебные пособия, которые напрямую наследуются при выполнении фильтров.UsernamePasswordAuthenticationFilter
, этот способ тоже осуществим, но после того, как мы разберемся в общем процессе работы, вы понимаете, что в этом нет необходимости, вам не нужно наследовать XXX, просто напишите фильтр и поставьте его в эту позицию.
Ну а после статьи о сертификации следующая статья — динамическая аутентификация, это моя первая статья в Наггетс, мой первый вывод знаний, и я надеюсь, что вы и дальше будете обращать внимание.
Каждый ваш лайк, подборка и комментарий - отличное подтверждение моих знаний.Если есть какие-то ошибки или сомнения в тексте, или какой-то совет для меня, вы можете оставить сообщение под областью комментариев и обсудить вместе.
Я ухо, человек, который всегда хотел заняться выводом знаний, увидимся в следующем выпуске.
Код для этой статьи:Адрес облака кода Адрес GitHub