Оauth2.0
Прежде всего, давайте разберёмся, что такое Оauth2.0. Говоря простым языком, Оauth2.0 — это протокол авторизации. Возможно, вы слышали, что Оauth2.0 используется для реализации единого входа и сторонней аутентификации. Что же такое авторизация?
Рассмотрим простой пример: человек А хочет войти в здание компании B, чтобы провести деловую встречу. Поскольку А не является сотрудником компании B, по соображениям безопасности он не может свободно входить в здание. Когда А подходит к стойке регистрации компании B, ему нужно объяснить цель своего визита и предъявить приглашение (доказательство доступа), после чего сотрудник стойки регистрации выдаст ему временный пропуск, который позволит ему войти в здание.
В этом примере у человека А нет пропуска, поэтому он не может войти в здание компании В. Сотрудник стойки регистрации выдаёт человеку А временный пропуск. Эта операция аналогична авторизации.
Таким образом, протокол авторизации Оauth 2.0 гарантирует, что стороннее программное обеспечение (третья сторона) может получить доступ к данным авторизованного лица только после получения разрешения.
1. Протокол авторизации Оauth2.0
Протокол авторизации Оauth2.0 включает в себя несколько механизмов предоставления разрешений:
Перед тем как понять механизмы предоставления разрешений, необходимо знать, что в системе Оauth 2.0 есть четыре роли: владелец ресурса, клиент, служба авторизации и защищённый ресурс.
— Владелец ресурса (может быть владельцем ресурса или иметь доступ к ресурсу). — Клиент (может пониматься как сторонняя система/программное обеспечение). — Служба авторизации (система проверки подлинности и авторизации). — Защищённый ресурс (ресурсы или данные, к которым пользователь имеет доступ в системе).
1.1. Механизм предоставления разрешений на основе кода
Участники механизма предоставления разрешений на основе кода: владелец ресурса, клиент, служба авторизации, защищённый ресурс.
Авторизация в этом сценарии позволяет стороннему программному обеспечению получать доступ к ресурсам владельца после получения кода авторизации и client_id и client_secret, полученных при регистрации.
Последовательность действий:
Почему необходимо генерировать код запроса и получать токен доступа access_token? Если исключить процесс получения токена доступа из последовательности действий, то после того, как пользователь подтвердит авторизацию, связь между владельцем ресурса и сервером авторизации будет установлена, а связь между владельцем ресурса и клиентом будет разорвана, и интерфейс останется на странице авторизации. Затем сервер авторизации напрямую отправит токен доступа клиенту, и клиент получит доступ к защищённому ресурсу с помощью токена доступа. Хотя данные были получены, как клиент может уведомить пользователя? Поэтому необходимо восстановить связь между пользователем и клиентом. Вот почему сервер авторизации генерирует код запроса, а затем перенаправляет клиента на страницу клиента.
Поскольку это так, зачем серверу авторизации отправлять токен доступа напрямую, вместо того чтобы перенаправить клиента? Во-первых, нельзя гарантировать, что перенаправление будет осуществляться по протоколу HTTPS, и не все клиенты поддерживают HTTPS. Отправка токена доступа таким образом увеличит риск кражи токена. Несмотря на то, что токену доступа требуется client_id, client_secret для проверки доступа к защищённым ресурсам, с точки зрения безопасности это не рекомендуется. В этом контексте роль кода запроса заключается в защите токена доступа от кражи.
Можно ли раскрыть код запроса?
Где хранить токен доступа? Если токен доступа нельзя раскрывать в браузере, где его хранить? Как клиент получает доступ к защищённым ресурсам?
На начальном этапе работы с Оauth2.0 я также был озадачен. Если токен доступа не раскрывается в браузере, куда его поместить? Я подумал, что после проверки кода запроса и client_id и secret для получения токена доступа можно сохранить токен доступа в localStorage, поскольку localStorage является постоянным хранилищем, но токен доступа имеет срок действия. По истечении срока действия токен доступа больше не сможет получить доступ к защищённым ресурсам.
1.2. Отличие между sessionStorage и localStorage
// Хранение данных
sessionStorage.setItem("name", "nameValue");
// Получение данных
sessionStorage.getItem("name");
// Удаление данных
sessionStorage.removeItem("name");
// Очистка всех данных
sessionStorage.clear();
// Хранение данных
localStorage.setItem("name", "nameValue");
// Получение данных
localStorage.getItem("name");
// Удаление данных
localStorage.removeItem("name");
Обратите внимание, что разные браузеры не могут совместно использовать информацию в localStorage или sessionStorage. Страницы и вкладки одного и того же домена и порта в одном браузере могут совместно использовать localStorage, но страницы и вкладки не могут совместно использовать sessionStorage. ### 2.2. Авторизация и получение access_token
Нажмите «Войти» и авторизуйтесь.
Получите код авторизации:
https://www.baidu.com/?code=SBkZt5
Нажмите кнопку «Авторизоваться».
Используйте Postman для тестирования запроса на получение access_token:
http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=torlesse002&client_secret=123456&code=SBkZt5&redirect_uri=https://baidu.com
OAuthClientConfig — конфигурация клиента OAuth2:
@Configuration
@EnableOAuth2Sso
public class OAuthClientConfig {
/**
* Определяет OAuth2RestTemplate. Можно прочитать конфигурацию oauth из файла application.yml и внедрить её в OAuth2ProtectedResourceDetails.
* @param oAuth2ClientContext
* @param details
* @return
*/
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oAuth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oAuth2ClientContext);
}
}
Конфигурация безопасности Spring:
@Configuration
@Order(200)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Разрешает доступ к /demo** и /login**. Все остальные пути требуют аутентификации.
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers( "/demo**","/login**")
.permitAll()
.antMatchers("/test/hello")
.anonymous()
.anyRequest()
.authenticated();
}
}
Контроллер для имитации сценария:
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
OAuth2RestTemplate restTemplate;
/**
* Используется для тестирования однократной аутентификации
* @param authentication
* @return
*/
@GetMapping("/userInfoPage")
public ModelAndView securedPage(OAuth2Authentication authentication) {
return new ModelAndView("userInfoPage").addObject("authentication", authentication);
}
/**
* Доступ к защищённым ресурсам
* @return
*/
@GetMapping("/remoteCall")
public String remoteCall() {
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://localhost:8081/user/name", String.class);
return responseEntity.getBody();
}
}
Ресурсная конфигурация сервера:
@Configuration
@EnableResourceServer//Запуск ресурсного сервера
@EnableGlobalMethodSecurity(prePostEnabled = true)//Запуск контроля доступа с помощью аннотаций
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
/**
* Объявляет идентификатор ресурсного сервера как torlesseservice и токен-хранилище как JWT.
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("torlesseservice").tokenStore(tokenStore());
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* Конфигурирует открытый ключ
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey = null;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
return converter;
}
/**
* Настраивает, что все запросы, кроме /user, могут быть анонимными.
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST,"/user/**").authenticated()
.antMatchers(HttpMethod.GET,"/user/**").authenticated()
.anyRequest().permitAll();
}
}
Здесь приведён неполный перевод. 2.4. Управление доступом на основе ролей
2.4.1. Постман имитирует доступ клиента к защищённым ресурсам
Используйте пользователя torlesse для получения токена, а затем используйте Postman для имитации доступа клиента к защищенным ресурсам.
http://localhost:8081/user/name
Postman используется в качестве имитирующего клиента для доступа к защищенным ресурсам:
http://localhost:8081/user/name
2.4.2. Доступ клиента к защищённым ресурсам
Доступ к странице через браузер: http://localhost:8083/torlesse/demo/remoteCall
Нажмите кнопку «Войти».
Вы успешно получили доступ к демонстрационным защищенным ресурсам.
2.4.3. Демонстрация единого входа
Подготовка
1.1 Измените файл конфигурации клиента application.yml:
server:
port: ${PORT:8083}
1.2 Настройте несколько клиентов:
Запустите приложение.
Проверка единого входа
Доступ к странице через браузер: http://localhost:8083/torlesse/demo/userInfoPage
Нажмите кнопку «Вход».
Доступ к той же странице через другой клиент: http://localhost:8082/torlesse/demo/userInfoPage
.
Единый вход работает успешно.
2.5. Пользовательский режим авторизации
Oauth2.0 имеет множество механизмов авторизации: механизм авторизации кода, механизм клиентских учетных данных, механизм учетных данных владельца ресурса (режим пароля) и неявный механизм авторизации.
В исходном коде можно увидеть реализацию четырех режимов, а также класс RefreshTokenGranter, который является классом обновления токенов и используется для обновления истекшего времени access_token.
Если вам нужно реализовать функции входа по мобильному телефону или сканирования QR-кода WeChat, как это сделать?
Мы можем наследовать от класса AbstractTokenGranter для реализации пользовательского режима авторизации.
Режим мобильного телефона с кодом подтверждения
Наследуйте от класса AbstractTokenGranter и реализуйте пользовательский режим входа с помощью мобильного телефона.
@Slf4j
public class SmsCodeGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "sms_code";
protected final AuthenticationManager authenticationManager;
protected final UserMapper userMapper;
public SmsCodeGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, UserMapper userMapper) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
this.userMapper = userMapper;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String telephone = parameters.get("telePhone");
String code = parameters.get("code");
if (StringUtils.isEmpty(telephone) || StringUtils.isEmpty(code)) {
throw new InvalidGrantException("参数错误.");
}
CheckParam checkParam = new CheckParam();
checkParam.setTelePhone(telephone);
``` **Пользователь user = userMapper.selectUserByCondition(checkParam.getTelePhone());**
log.info("telephone = {}, code = {}, user = {}", телефон, код, JSON.toJSONString(пользователь)); // 根据 мобильному номеру телефона запрашиваем информацию о пользователе
if (пользователь == null) {
выбросить новое исключение InvalidGrantException("Ошибка в заполнении номера мобильного телефона.");
}
Authentication userAuth = new TelePhoneAuthenticationToken(пользователь.имя_пользователя(), пользователь.пароль(), телефон, код);
((AbstractAuthenticationToken) userAuth).setDetails(параметры);
try {
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException var8) {
выбросить новое исключение InvalidGrantException("Текущий пользователь уже заблокирован, пожалуйста, обратитесь в службу поддержки.");
} catch (BadCredentialsException var9) {
выбросить новое исключение InvalidGrantException("Информация о пользователе получена с ошибкой, пожалуйста, убедитесь, что вы зарегистрированы.");
} catch (InternalAuthenticationServiceException var10) {
выбросить новое исключение InvalidGrantException("Проверка кода не удалась.");
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(клиент, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
выбросить новое исключение InvalidGrantException("Не удалось аутентифицировать пользователя: " + телефон);
}
**Класс телефона AuthenticationToken расширяет UsernamePasswordAuthenticationToken**
private String telePhone;
private String code;
/**
* @param principal имя пользователя
*/
public TelePhoneAuthenticationToken(Object principal, Object credentials, String telePhone, String code) {
super(principal, credentials);
setAuthenticated(false);
this.telePhone = telePhone;
this.code = code;
}
public String getTelePhone() {
вернуть telePhone;
}
public String getCode() {
вернуть code;;
}
#### 2.5.2 Модификация конфигурации сервера авторизации
AuthorizationServerConfig класс конфигурации сервера авторизации добавляет TokenGranter
/**
* Инициализация всех TokenGranters
*/
private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {
ClientDetailsService clientDetails = endpoints.getClientDetailsService();
AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
List<TokenGranter> tokenGranters = new ArrayList<>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
clientDetails, requestFactory));
tokenGranters.add(new SmsCodeGranter(authenticationManager, endpoints.getTokenServices(),
endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), userMapper));
}
return tokenGranters;
}
Модификация конфигурации сервера авторизации, добавление следующих элементов
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer()));
// Инициализируем все TokenGranters и устанавливаем тип CompositeTokenGranter
List tokenGranters = getDefaultTokenGranters(endpoints);
endpoints.approvalStore(approvalStore())
tokenGranter(new CompositeTokenGranter(tokenGranters))
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
#### 2.5.3 Демонстрация проверки в режиме авторизации с использованием верификации по номеру телефона
Запрос от клиента, имитированный в Postman:
```http://localhost:8080/oauth/token?grant_type=sms_code&client_id=torlesse004&client_secret=123456&telePhone=12345678999&code=123123```
[Рисунок image-20220414225932247.png]
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )