Широ элегантно интегрируется в SpringBoot со стартером.

Spring Boot Java Spring

Широ элегантно интегрируется в SpringBoot со стартером.

Большинство статей, найденных в Интернете, представляют собой методы интеграции в SpringMVC. В этой статье представлены три моих идеи интеграции: 1. Полностью использовать аннотации 2. Полностью использовать конфигурацию URL-адресов 3. Совмещать конфигурацию URL-адресов и аннотаций, конфигурация URL-адресов отвечает за контроль аутентификации, а аннотации отвечают за контроль разрешений. Каждый из трех методов имеет свои преимущества и недостатки, и их необходимо использовать в сценариях практического применения.

код

Talk is cheap, show you my code: elegant-shiro-boot
Этот проект построен с использованием gradle и имеет три подпроекта:

  • demo1 демонстрирует, что для аутентификации и авторизации используются только аннотации
  • demo2 демонстрирует, что для аутентификации и авторизации используется только конфигурация URL
  • demo3 демонстрирует комбинацию двух методов: конфигурация URL отвечает за контроль аутентификации, а конфигурация аннотаций отвечает за контроль авторизации.

Как интегрировать

Пожалуйста, смотрите ссылку на официальном сайте shiro об интеграции springboot с shiro:Integrating Apache Shiro into Spring-Boot Applications

Самое смешное, что я зашел на официальный сайт, чтобы найти его сам, и я не смог найти документацию на этой странице, но нашел ее через google.

Введение в этот документ также довольно простое. Нам просто нужно следовать документации и импортироватьshiro-spring-boot-starter, а затем внедрить один из наших пользовательских в контейнер SpringRealm, Широ может знать, как получить информацию о пользователе для обработки в этом мире.鉴权(Authentication), как получить информацию о роли пользователя и разрешениях для обработки授权(Authorization).

ps: Аутентификацию можно понимать как процесс определения того, вошел ли пользователь в систему, а авторизацию можно понимать как процесс определения наличия прав доступа у вошедшего в систему пользователя.

Процесс интеграции:
1. Представляем стартер.Я использую gradle для сборки проекта, и maven также может ввести соответствующие зависимости:

dependencies {
    //spring boot的starter
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.boot:spring-boot-starter-aop'
    compile 'org.springframework.boot:spring-boot-devtools'
    testCompile 'org.springframework.boot:spring-boot-starter-test'
    //shiro
    compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.4.0'
}

2. Напишите пользовательскую область

User.java (для других моделей RBAC см. класс в пакете com.abc.entity для кода на github)

public class User {

    private Long uid;       // 用户id
    private String uname;   // 登录名,不可改
    private String nick;    // 用户昵称,可改
    private String pwd;     // 已加密的登录密码
    private String salt;    // 加密盐值
    private Date created;   // 创建时间
    private Date updated;   // 修改时间
    private Set<String> roles = new HashSet<>();    //用户所有角色值,用于shiro做角色权限的判断
    private Set<String> perms = new HashSet<>();    //用户所有权限值,用于shiro做资源权限的判断
    //getters and setters...
}

UserService.java

@Service
public class UserService {

    /**
     * 模拟查询返回用户信息
     * @param uname
     * @return
     */
    public User findUserByName(String uname){
        User user = new User();
        user.setUname(uname);
        user.setNick(uname+"NICK");
        user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密码明文是123456
        user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密码的盐值
        user.setUid(new Random().nextLong());//随机分配一个id
        user.setCreated(new Date());
        return user;
    }
}

RoleService.java

@Service
public class RoleService {

    /**
     * 模拟根据用户id查询返回用户的所有角色,实际查询语句参考:
     * SELECT r.rval FROM role r, user_role ur
     * WHERE r.rid = ur.role_id AND ur.user_id = #{userId}
     * @param uid
     * @return
     */
    public Set<String> getRolesByUserId(Long uid){
        Set<String> roles = new HashSet<>();
        //三种编程语言代表三种角色:js程序员、java程序员、c++程序员
        roles.add("js");
        roles.add("java");
        roles.add("cpp");
        return roles;
    }

}

PermService.java

@Service
public class PermService {

    /**
     * 模拟根据用户id查询返回用户的所有权限,实际查询语句参考:
     * SELECT p.pval FROM perm p, role_perm rp, user_role ur
     * WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id
     * AND ur.user_id = #{userId}
     * @param uid
     * @return
     */
    public Set<String> getPermsByUserId(Long uid){
        Set<String> perms = new HashSet<>();
        //三种编程语言代表三种角色:js程序员、java程序员、c++程序员
        //js程序员的权限
        perms.add("html:edit");
        //c++程序员的权限
        perms.add("hardware:debug");
        //java程序员的权限
        perms.add("mvn:install");
        perms.add("mvn:clean");
        perms.add("mvn:test");
        return perms;
    }

}

CustomRealm.java


/**
 * 这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermService permService;

    //告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
    {
        //设置用于匹配密码的CredentialsMatcher
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        hashMatcher.setStoredCredentialsHexEncoded(false);
        hashMatcher.setHashIterations(1024);
        this.setCredentialsMatcher(hashMatcher);
    }


    //定义如何获取用户的角色和权限的逻辑,给shiro做权限判断
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        User user = (User) getAvailablePrincipal(principals);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        System.out.println("获取角色信息:"+user.getRoles());
        System.out.println("获取权限信息:"+user.getPerms());
        info.setRoles(user.getRoles());
        info.setStringPermissions(user.getPerms());
        return info;
    }

    //定义如何获取用户信息的业务逻辑,给shiro做登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();

        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }

        User userDB = userService.findUserByName(username);


        if (userDB == null) {
            throw new UnknownAccountException("No account found for admin [" + username + "]");
        }

        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
        Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
        Set<String> perms = permService.getPermsByUserId(userDB.getUid());
        userDB.getRoles().addAll(roles);
        userDB.getPerms().addAll(perms);

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
        if (userDB.getSalt() != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
        }

        return info;

    }

}

3. Используйте аннотацию или конфигурацию URL для управления аутентификацией и авторизацией.

Пожалуйста, обратитесь к примеру на официальном сайте:

//url配置
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    
    // logged in users with the 'admin' role
    chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
    
    // logged in users with the 'document:read' permission
    chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
    
    // all other paths require a logged in user
    chainDefinition.addPathDefinition("/**", "authc");
    return chainDefinition;
}
//注解配置
@RequiresPermissions("document:read")
public void readDocument() {
    ...
}

4. Устранить ошибку совместного использования конфигурации Spring AOP и аннотаций. Если вы введете стартер spring aop при использовании конфигурации аннотации shiro, возникнет странная проблема, в результате чего запрос аннотации shiro не может быть сопоставлен, вам необходимо добавить следующую конфигурацию:

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

Идея 1: Используйте аннотации только для управления аутентификацией и авторизацией

Преимущество использования аннотаций заключается в том, что элемент управления детализирован и очень подходит для управления разрешениями на основе ресурсов.

Для управления разрешениями на основе ресурсов рекомендуется прочитать эту статью:The New RBAC: Resource-Based Access Control

Это очень просто с аннотациями. Нам нужно только использовать конфигурацию URL-адреса, чтобы настроить его так, чтобы к пути запроса можно было получить анонимный доступ:

    //在 ShiroConfig.java 中的代码:
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        // 由于demo1展示统一使用注解做访问控制,所以这里配置所有请求路径都可以匿名访问
        chain.addPathDefinition("/**", "anon"); // all paths are managed via annotations

        // 这另一种配置方式。但是还是用上面那种吧,容易理解一点。
        // or allow basic authentication, but NOT require it.
        // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
        return chain;
    }

Затем используйте аннотации, предоставленные shiro в классе контроллера, для управления:

аннотация Функции
@RequiresGuest Только туристы могут посетить
@RequiresAuthentication Для доступа требуется логин
@RequiresUser Вошедшие в систему или "запомнившие меня" пользователи могут получить доступ
@RequiresRoles Вошедший в систему пользователь должен иметь указанную роль для доступа
@RequiresPermissions Вошедшим в систему пользователям необходимы определенные разрешения для доступа

Пример кода: (Подробнее см. в demo1 кода github)

/**
 * created by CaiBaoHong at 2018/4/18 15:51<br>
 *     测试shiro提供的注解及功能解释
 */
@RestController
@RequestMapping("/t1")
public class Test1Controller {

    // 由于TestController类上没有加@RequiresAuthentication注解,
    // 不要求用户登录才能调用接口。所以hello()和a1()接口都是可以匿名访问的
    @GetMapping("/hello")
    public String hello() {
        return "hello spring boot";
    }

    // 游客可访问,这个有点坑,游客的意思是指:subject.getPrincipal()==null
    // 所以用户在未登录时subject.getPrincipal()==null,接口可访问
    // 而用户登录后subject.getPrincipal()!=null,接口不可访问
    @RequiresGuest
    @GetMapping("/guest")
    public String guest() {
        return "@RequiresGuest";
    }

    // 已登录用户才能访问,这个注解比@RequiresUser更严格
    // 如果用户未登录调用该接口,会抛出UnauthenticatedException
    @RequiresAuthentication
    @GetMapping("/authn")
    public String authn() {
        return "@RequiresAuthentication";
    }

    // 已登录用户或“记住我”的用户可以访问
    // 如果用户未登录或不是“记住我”的用户调用该接口,UnauthenticatedException
    @RequiresUser
    @GetMapping("/user")
    public String user() {
        return "@RequiresUser";
    }

    // 要求登录的用户具有mvn:build权限才能访问
    // 由于UserService模拟返回的用户信息中有该权限,所以这个接口可以访问
    // 如果没有登录,UnauthenticatedException
    @RequiresPermissions("mvn:install")
    @GetMapping("/mvnInstall")
    public String mvnInstall() {
        return "mvn:install";
    }

    // 要求登录的用户具有mvn:build权限才能访问
    // 由于UserService模拟返回的用户信息中【没有】该权限,所以这个接口【不可以】访问
    // 如果没有登录,UnauthenticatedException
    // 如果登录了,但是没有这个权限,会报错UnauthorizedException
    @RequiresPermissions("gradleBuild")
    @GetMapping("/gradleBuild")
    public String gradleBuild() {
        return "gradleBuild";
    }

    // 要求登录的用户具有js角色才能访问
    // 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
    // 如果没有登录,UnauthenticatedException
    @RequiresRoles("js")
    @GetMapping("/js")
    public String js() {
        return "js programmer";
    }

    // 要求登录的用户具有js角色才能访问
    // 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
    // 如果没有登录,UnauthenticatedException
    // 如果登录了,但是没有该角色,会抛出UnauthorizedException
    @RequiresRoles("python")
    @GetMapping("/python")
    public String python() {
        return "python programmer";
    }

}

Идея 2: Используйте только конфигурацию URL для управления аутентификацией и авторизацией

Широ предоставляет и ряд фильтров по умолчанию, мы можем использовать эти фильтры для настройки разрешений для управления указанным URL-адресом:

аббревиатура конфигурации соответствующий фильтр Функции
anon AnonymousFilter Указанный URL-адрес может быть доступен анонимно
authc FormAuthenticationFilter Указанный URL-адрес требует входа в форму, которая будет получена из запроса по умолчанию.username,password,rememberMeДождитесь параметров и попробуйте войти. Если вы не можете войти, он перейдет на путь, настроенный loginUrl. Мы также можем использовать этот фильтр в качестве логики входа по умолчанию, но обычно мы сами пишем логику входа в контроллер.Если мы пишем ее сами, информацию, возвращаемую по ошибке, можно настроить.
authcBasic BasicHttpAuthenticationFilter Для указания URL-адреса требуется базовый вход в систему
logout LogoutFilter Фильтр выхода, настройте указанный URL-адрес для реализации функции выхода из системы, что очень удобно.
noSessionCreation NoSessionCreationFilter Отключить создание сеанса
perms PermissionsAuthorizationFilter Требуется указанное разрешение на доступ
port PortFilter Необходимо указать порт для доступа
rest HttpMethodPermissionFilter Преобразуйте метод http-запроса в соответствующий глагол, чтобы создать строку разрешения, что не имеет особого смысла. Мне интересно лично посмотреть комментарии к исходному коду.
roles RolesAuthorizationFilter Требуется указанная роль для доступа
ssl SslFilter Требуется HTTPS-запрос для доступа
user UserFilter Для доступа требуется авторизованный пользователь или пользователь с функцией «запомнить меня».

использовать в пружинном контейнереShiroFilterChainDefinitionдля контроля аутентификации и авторизации всех URL-адресов. Преимущество заключается в том, что степень детализации конфигурации велика, а проверка подлинности и авторизация выполняются несколькими контроллерами. Ниже приведен пример, вы можете увидеть демо2 кода github для деталей:

@Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();

        /**
         * 这里小心踩坑!我在application.yml中设置的context-path: /api/v1
         * 但经过实际测试,过滤器的过滤路径,是context-path下的路径,无需加上"/api/v1"前缀
         */

        //访问控制
        chain.addPathDefinition("/user/login", "anon");//可以匿名访问
        chain.addPathDefinition("/page/401", "anon");//可以匿名访问
        chain.addPathDefinition("/page/403", "anon");//可以匿名访问
        chain.addPathDefinition("/t4/hello", "anon");//可以匿名访问

        chain.addPathDefinition("/t4/changePwd", "authc");//需要登录
        chain.addPathDefinition("/t4/user", "user");//已登录或“记住我”的用户可以访问
        chain.addPathDefinition("/t4/mvnBuild", "authc,perms[mvn:install]");//需要mvn:build权限
        chain.addPathDefinition("/t4/gradleBuild", "authc,perms[gradle:build]");//需要gradle:build权限
        chain.addPathDefinition("/t4/js", "authc,roles[js]");//需要js角色
        chain.addPathDefinition("/t4/python", "authc,roles[python]");//需要python角色

        // shiro 提供的登出过滤器,访问指定的请求,就会执行登录,默认跳转路径是"/",或者是"shiro.loginUrl"配置的内容
        // 由于application-shiro.yml中配置了 shiro:loginUrl: /page/401,返回会返回对应的json内容
        // 可以结合/user/login和/t1/js接口来测试这个/t4/logout接口是否有效
        chain.addPathDefinition("/t4/logout", "anon,logout");

        //其它路径均需要登录
        chain.addPathDefinition("/**", "authc");

        return chain;
    }

Идея 3. Объединение этих двух факторов: конфигурация URL-адреса управляет аутентификацией, а аннотация — авторизацией.

Лично мне очень нравится метод аннотации. Тем не менее, гибкая комбинация двух методов настройки является лучшей практикой для адаптации к различным сценариям применения. Использование только аннотаций или только конфигурации URL потребует утомительной работы.

Я даю два сценария:
Сцена 1
Если я пишу систему управления фоном системы, а мой фон Java — это фон, который просто возвращает данные json, он не будет выполнять работу по переходу на страницу. Тогда наша система фонового управления обычно требует входа в систему для доступа ко всем интерфейсам. Если используются только аннотации, мне нужно добавить на каждый контроллер@RequiresAuthenticationЧтобы объявить, что для доступа к каждому методу в каждом контроллере необходимо войти в систему. Это кажется немного хлопотным, и если вы добавите контроллер в будущем, вам все равно придется добавить эту аннотацию, если вы забудете ее добавить, вы совершите ошибку. В настоящее время вы можете использовать метод конфигурации URL для настройки всех запросов на вход для доступа:chain.addPathDefinition("/**", "authc");

сцена 2
Предположим, я пишу на ресепшн торгового центра, и мой java-бэкенд — это бэкенд, который просто возвращает json-данные, но среди этих интерфейсов под одним и тем же контроллером к некоторым можно получить доступ анонимно, а к некоторым можно получить доступ только войдя в систему. Некоторым требуются определенные роли и разрешения для доступа. Если используется только конфигурация URL-адресов, необходимо настроить каждый URL-адрес, и его легко настроить неправильно, а степень детализации нелегко контролировать.

Итак, мои мысли:用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制.

Ниже приведен пример кода (подробности см. в demo3 кода github): ShiroConfig.java

@Configuration
public class ShiroConfig {

    //注入自定义的realm,告诉shiro如何获取用户信息来做登录或权限控制
    @Bean
    public Realm realm() {
        return new CustomRealm();
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        creator.setUsePrefix(true);
        return creator;
    }

    /**
     * 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
     * 这里只做鉴权,不做权限控制,因为权限用注解来做。
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        //哪些请求可以匿名访问
        chain.addPathDefinition("/user/login", "anon");
        chain.addPathDefinition("/page/401", "anon");
        chain.addPathDefinition("/page/403", "anon");
        chain.addPathDefinition("/t5/hello", "anon");
        chain.addPathDefinition("/t5/guest", "anon");

        //除了以上的请求外,其它请求都需要登录
        chain.addPathDefinition("/**", "authc");
        return chain;
    }
}

PageController.java

@RestController
@RequestMapping("/page")
public class PageController {

    // shiro.loginUrl映射到这里,我在这里直接抛出异常交给GlobalExceptionHandler来统一返回json信息,
    // 您也可以在这里json,不过这样子就跟GlobalExceptionHandler中返回的json重复了。
    @RequestMapping("/401")
    public Json page401() {
        throw new UnauthenticatedException();
    }

    // shiro.unauthorizedUrl映射到这里。由于demo3统一约定了url方式只做鉴权控制,不做权限访问控制,
    // 也就是说在ShiroConfig中如果没有roles[js],perms[mvn:install]这样的权限访问控制配置的话,
    // 是不会跳转到这里的。
    @RequestMapping("/403")
    public Json page403() {
        throw new UnauthorizedException();
    }

    @RequestMapping("/index")
    public Json pageIndex() {
        return new Json("index",true,1,"index page",null);
    }


}

GlobalExceptionHandler.java

/**
 * 统一捕捉shiro的异常,返回给前台一个json信息,前台根据这个信息显示对应的提示,或者做页面的跳转。
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //不满足@RequiresGuest注解时抛出的异常信息
    private static final String GUEST_ONLY = "Attempting to perform a guest-only operation";


    @ExceptionHandler(ShiroException.class)
    @ResponseBody
    public Json handleShiroException(ShiroException e) {
        String eName = e.getClass().getSimpleName();
        log.error("shiro执行出错:{}",eName);
        return new Json(eName, false, Codes.SHIRO_ERR, "鉴权或授权过程出错", null);
    }

    @ExceptionHandler(UnauthenticatedException.class)
    @ResponseBody
    public Json page401(UnauthenticatedException e) {
        String eMsg = e.getMessage();
        if (StringUtils.startsWithIgnoreCase(eMsg,GUEST_ONLY)){
            return new Json("401", false, Codes.UNAUTHEN, "只允许游客访问,若您已登录,请先退出登录", null)
                    .data("detail",e.getMessage());
        }else{
            return new Json("401", false, Codes.UNAUTHEN, "用户未登录", null)
                    .data("detail",e.getMessage());
        }
    }

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public Json page403() {
        return new Json("403", false, Codes.UNAUTHZ, "用户没有访问权限", null);
    }

}

TestController.java

@RestController
@RequestMapping("/t5")
public class Test5Controller {

    // 由于ShiroConfig中配置了该路径可以匿名访问,所以这接口不需要登录就能访问
    @GetMapping("/hello")
    public String hello() {
        return "hello spring boot";
    }

    // 如果ShiroConfig中没有配置该路径可以匿名访问,所以直接被登录过滤了。
    // 如果配置了可以匿名访问,那这里在没有登录的时候可以访问,但是用户登录后就不能访问
    @RequiresGuest
    @GetMapping("/guest")
    public String guest() {
        return "@RequiresGuest";
    }

    @RequiresAuthentication
    @GetMapping("/authn")
    public String authn() {
        return "@RequiresAuthentication";
    }

    @RequiresUser
    @GetMapping("/user")
    public String user() {
        return "@RequiresUser";
    }

    @RequiresPermissions("mvn:install")
    @GetMapping("/mvnInstall")
    public String mvnInstall() {
        return "mvn:install";
    }

    @RequiresPermissions("gradleBuild")
    @GetMapping("/gradleBuild")
    public String gradleBuild() {
        return "gradleBuild";
    }


    @RequiresRoles("js")
    @GetMapping("/js")
    public String js() {
        return "js programmer";
    }


    @RequiresRoles("python")
    @GetMapping("/python")
    public String python() {
        return "python programmer";
    }

}