1. Общ преглед
В този урок ще обсъдим как да накараме нашата реализация Spring OAuth2 да използва JSON Web Tokens.
Също така продължаваме да изграждаме върху Spring REST API + OAuth2 + Angular статия в тази серия OAuth.
2. Сървърът за упълномощаване OAuth2
Преди това стекът Spring OAuth предлагаше възможност за настройка на сървър за оторизация като пролетно приложение. След това трябваше да го конфигурираме да използва JwtTokenStore, за да можем да използваме JWT токени.
Въпреки това стекът OAuth е остарял от Spring и сега ще използваме Keycloak като наш сървър за оторизация.
Така че този път ще настроим нашия Authorization Server като вграден сървър Keycloak в приложение Spring Spring . Той издава JWT маркери по подразбиране, така че няма нужда от друга конфигурация в това отношение.
3. Ресурсен сървър
Сега нека да разгледаме как да конфигурираме нашия Resource Server да използва JWT.
Ще направим това във файл application.yml :
server: port: 8081 servlet: context-path: /resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs
JWT включват цялата информация в рамките на жетона. Така че Resource Server трябва да провери подписа на маркера, за да се увери, че данните не са модифицирани. В jwk-набор-URI Имотът съдържа публичния ключ , че сървърът може да използва за тази цел .
На емитента-URI точки собственост върху основата на разрешението за сървър URI, който също може да се използва за проверка на МКС претенцията, като допълнителна мярка за сигурност.
Също така, ако свойството jwk-set-uri не е зададено, Resource Server ще се опита да използва издаващия ui за определяне на местоположението на този ключ от крайната точка на метаданните на Authorization Server.
Важното е, че добавянето на издателя-uri свойство указва, че трябва да работи сървърът за упълномощаване, преди да можем да стартираме приложението Resource Server .
Сега нека видим как можем да конфигурираме поддръжката на JWT, използвайки Java конфигурация:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**") .hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/api/foos") .hasAuthority("SCOPE_write") .anyRequest() .authenticated() .and() .oauth2ResourceServer() .jwt(); } }
Тук заместваме конфигурацията по подразбиране Http Security. Затова трябва да посочим изрично, че искаме това да се държи като Resource Server и че ще използваме форматирани в JWT маркери за достъп, използвайки съответно методите oauth2ResourceServer () и jwt () .
Горната конфигурация на JWT е това, което ни предоставя стандартният екземпляр Spring Boot. Това също може да бъде персонализирано, както ще видим скоро.
4. Персонализирани искове в жетона
Нека сега настроим някаква инфраструктура, за да можем да добавим няколко потребителски искания в маркера за достъп, върнат от сървъра за оторизация . Стандартните искове, предоставени от рамката, са добре и добре, но през повечето време ще ни трябва допълнителна информация в маркера, за да се използва от страна на клиента.
Нека вземем пример за потребителско искане, организация , което ще съдържа името на дадена потребителска организация.
4.1. Конфигурация на сървър за оторизация
За това трябва да добавим няколко конфигурации към нашия файл за дефиниция на царството, baeldung-realm.json :
- Добавете организация на атрибутите към нашия потребител [защитен по имейл] :
"attributes" : { "organization" : "baeldung" },
- Добавете ProtocolMapper, наречена организация към конфигурацията на jwtClient :
"protocolMappers": [{ "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1", "name": "organization", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "organization", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "organization", "jsonType.label": "String" } }],
За самостоятелна настройка на Keycloak това може да се направи и с помощта на Admin console.
Освен това е важно да запомните, че конфигурацията на JSON по-горе е специфична за Keycloak и може да се различава за другите OAuth сървъри .
С тази нова конфигурация, която работи и ще получим допълнителен атрибут организация = baeldung в полезния товар на маркера за [имейл защитен] :
{ jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e" exp: 1585242462 nbf: 0 iat: 1585242162 iss: "//localhost:8083/auth/realms/baeldung" sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f" typ: "Bearer" azp: "jwtClient" auth_time: 1585242162 session_state: "384ca5cc-8342-429a-879c-c15329820006" acr: "1" scope: "profile write read" organization: "baeldung" preferred_username: "[email protected]" }
4.2. Използвайте маркера за достъп в Angular Client
След това ще искаме да използваме информацията за жетоните в нашето приложение за Angular Client. За това ще използваме библиотеката angular2-jwt.
Ще използваме искането на организацията в нашия AppService и ще добавим функция getOrganization :
getOrganization(){ var token = Cookie.get("access_token"); var payload = this.jwtHelper.decodeToken(token); this.organization = payload.organization; return this.organization; }
Тази функция използва JwtHelperService от библиотеката angular2-jwt, за да декодира маркера за достъп и да получи нашето потребителско искане. Сега всичко, което трябва да направим, е да го покажем в нашия AppComponent :
@Component({ selector: 'app-root', template: ` Spring Security Oauth - Authorization Code {{organization}}
` }) export class AppComponent implements OnInit { public organization = ""; constructor(private service: AppService) { } ngOnInit() { this.organization = this.service.getOrganization(); } }
5. Достъп до допълнителни искове в Resource Server
Но как можем да получим достъп до тази информация от страна на Resource Server?
5.1. Достъп до искания за сървър за удостоверяване
Това е много проста: ние просто трябва да го извлече от org.springframework.security.oauth2.jwt.Jwt е AuthenticationPrincipal , тъй като ние ще направим за всеки друг атрибут в UserInfoController :
@GetMapping("/user/info") public Map getUserInfo(@AuthenticationPrincipal Jwt principal) { Map map = new Hashtable(); map.put("user_name", principal.getClaimAsString("preferred_username")); map.put("organization", principal.getClaimAsString("organization")); return Collections.unmodifiableMap(map); }
5.2. Конфигурация за добавяне / премахване / преименуване на искове
Ами сега, ако искаме да добавим още твърдения от страна на Resource Server? Или да премахнете или преименувате някои?
Да приемем, че искаме да модифицираме искането на организацията , идващо от сървъра за удостоверяване, за да получим стойността с главни букви. Освен това, ако искът не присъства на потребител, трябва да зададем неговата стойност като неизвестна .
За да постигнем това, първо ще трябва да добавим клас, който реализира интерфейса на конвертора и използва MappedJwtClaimSetConverter за конвертиране на искове :
public class OrganizationSubClaimAdapter implements Converter
На второ място, в нашия SecurityConfig клас, трябва да добавите нашия собствен JwtDecoder инстанция да замени този, предоставен от пролетния Boot и да ни OrganizationSubClaimAdapter като му претенции конвертор :
@Bean public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( properties.getJwt().getJwkSetUri()).build(); jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter()); return jwtDecoder; }
Now when we hit our /user/info API for the user [email protected], we'll get the organization as UNKNOWN.
Note that overriding the default JwtDecoder bean configured by Spring Boot should be done carefully to ensure all the necessary configuration is still included.
6. Loading Keys From a Java Keystore
In our previous configuration, we used the Authorization Server's default public key to verify our token's integrity.
We can also use a keypair and certificate stored in a Java Keystore file to do the signing process.
6.1. Generate JKS Java KeyStore File
Let's first generate the keys – and more specifically a .jks file – using the command line tool keytool:
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
The command will generate a file called mytest.jks which contains our keys – the Public and Private keys.
Also make sure keypass and storepass are the same.
6.2. Export Public Key
Next, we need to export our Public key from generated JKS, we can use the following command to do so:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
A sample response will look like this:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2 /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3 DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK eQIDAQAB -----END PUBLIC KEY----- -----BEGIN CERTIFICATE----- MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1 czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2 MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3 1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0 yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp /J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF lLFCUGhA7hxn2xf3x1JW -----END CERTIFICATE-----
6.3. Maven Configuration
Next, we don't want the JKS file to be picked up by the maven filtering process – so we'll make sure to exclude it in the pom.xml:
src/main/resources true *.jks
If we're using Spring Boot, we need to make sure that our JKS file is added to application classpath via the Spring Boot Maven Plugin – addResources:
org.springframework.boot spring-boot-maven-plugin true
6.4. Authorization Server
Now, we will configure Keycloak to use our Keypair from mytest.jks, by adding it to the realm definition JSON file's KeyProvider section as follows:
{ "id": "59412b8d-aad8-4ab8-84ec-e546900fc124", "name": "java-keystore", "providerId": "java-keystore", "subComponents": {}, "config": { "keystorePassword": [ "mypass" ], "keyAlias": [ "mytest" ], "keyPassword": [ "mypass" ], "active": [ "true" ], "keystore": [ "src/main/resources/mytest.jks" ], "priority": [ "101" ], "enabled": [ "true" ], "algorithm": [ "RS256" ] } },
Here we have set the priority to 101, greater than any other Keypair for our Authorization Server, and set active to true. This is done to ensure that our Resource Server would pick this particular Keypair from the jwk-set-uri property we specified earlier.
Отново, тази конфигурация е специфична за Keycloak и може да се различава за други реализации на OAuth Server.
7. Заключение
В тази кратка статия се фокусирахме върху настройването на нашия проект Spring Security OAuth2 за използване на JSON Web Tokens.
Пълното изпълнение на този урок може да бъде намерено в над на GitHub.