3.9.11.3. Собственный механизм аутентификации

Различные механизмы аутентификации могут предоставлять токен по ключу, по ссылке, по логину и паролю LDAP и т.д. Стандартный механизм аутентификации в REST API изменить нельзя, но можно создать свой механизм. Для этого необходимо создать REST-контроллер, который предоставит свой URL для входа в приложение.

В этом примере мы рассмотрим механизм аутентификации, позволяющий получить OAuth-токен по промо-коду. За основу возьмём приложение, содержащее сущность Coupon (Купон) с атрибутом code (промо-код). Значение этого атрибута мы будем передавать в качестве параметра аутентификации в GET-запросе.

  1. Создайте сущность Coupon и добавьте ей атрибут code:

    @Column(name = "CODE", unique = true, length = 4)
    protected String code;
  2. Создайте нового пользователя с логином promo-user, от лица которого будет выполняться аутентификация по промо-коду.

  3. В корневом каталоге модуля web (com.company.demo) создайте новый файл конфигурации Spring rest-dispatcher-spring.xml со следующим содержанием:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
        <context:component-scan base-package="com.company.demo.web.rest"/>
    
    </beans>
  4. Ссылку на этот файл укажите в свойстве приложения cuba.restSpringContextConfig в файле modules/web/src/web-app.properties:

    cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
  5. Создайте пакет rest в корневом каталоге модуля web, а в нём - свой контроллер Spring MVC. В контроллере используйте бин OAuthTokenIssuer, который позволяет сгенерировать и выдать REST API токен после аутентификации:

    @RestController
    @RequestMapping("auth-code")
    public class AuthCodeController {
        @Inject
        private OAuthTokenIssuer oAuthTokenIssuer;
        @Inject
        private LoginService loginService;
        @Inject
        private Configuration configuration;
        @Inject
        private DataManager dataManager;
        @Inject
        private MessageTools messageTools;
    
        // here we check secret code and issue token using OAuthTokenIssuer
        @RequestMapping(method = RequestMethod.GET)
        public ResponseEntity get(@RequestParam("code") String authCode) {
            // obtain system session to be able to call middleware services
            WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);
            UserSession systemSession;
            try {
                systemSession = loginService.getSystemSession(webAuthConfig.getTrustedClientPassword());
            } catch (LoginException e) {
                throw new RuntimeException("Error during system auth");
            }
    
            // set security context
            AppContext.setSecurityContext(new SecurityContext(systemSession));
            try {
                // find coupon with code
                LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class)
                        .setQuery(LoadContext.createQuery("select c from demo$Coupon c where c.code = :code")
                                .setParameter("code", authCode));
    
                if (dataManager.load(loadContext) == null) {
                    // if coupon is not found - code is incorrect
                    return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST);
                }
    
                // generate token for "promo-user"
                OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult =
                        oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap());
                OAuth2AccessToken accessToken = tokenResult.getAccessToken();
    
                // set security HTTP headers to prevent browser caching of security token
                HttpHeaders headers = new HttpHeaders();
                headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
                headers.set(HttpHeaders.PRAGMA, "no-cache");
                return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
            } finally {
                // clean up security context
                AppContext.setSecurityContext(null);
            }
        }
    
        // POJO for JSON error messages
        public static class ErrorInfo implements Serializable {
            private String error;
            private String error_description;
    
            public ErrorInfo(String error, String error_description) {
                this.error = error;
                this.error_description = error_description;
            }
    
            public String getError() {
                return error;
            }
    
            public String getError_description() {
                return error_description;
            }
        }
    }
  6. Исключите пакет rest из сканирования в модулях web/core: это необходимо, так как бин OAuthTokenIssuer доступен только внутри контекста REST API, и сканирование его в контексте приложения будет вызывать ошибку.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/>
    </context:component-scan>
  7. Теперь пользователи могут получать код доступа OAuth2 через обычный запрос GET HTTP, передавая значение промо-кода в параметре code:

    http://localhost:8080/app/rest/auth-code?code=A325

    Результат:

    {"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}

    Теперь полученный access token нужно передавать в REST API, как описано в общей документации.