Научу вас Широ интегрировать SpringBoot и избегать всевозможных ям

задняя часть Безопасность Shiro Realm

Недавно я работал над фреймворком безопасности Shiro и нашел несколько статей в блогах в Интернете, но когда я реализовал его сам, я столкнулся с различными ямами, и мне нужна была различная информация, чтобы увидеть исходный код и различные тесты. Затем эта статья научит вас, как интегрировать Shiro в SpringBoot и избежать некоторых ловушек.На этот раз реализованы базовые права входа и роли.В следующих статьях также будут объясняться другие функции, такие как«Научите вас Широ + SpringBoot интегрировать JWT»

Прикрепите исходный код:Github.com/howie Yuan / есть ...

Пакет зависимостей

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.3.2</version>
</dependency>

Таблица базы данных

Сохраняйте все простым, пользовательская таблица пользователей и таблица ролей ролей

user

role

Классы, связанные с Широ

Класс конфигурации Широ

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        // 设置无权限时跳转的 url;
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //游客,开发权限
        filterChainDefinitionMap.put("/guest/**", "anon");
        //用户,需要角色权限 “user”
        filterChainDefinitionMap.put("/user/**", "roles[user]");
        //管理员,需要角色权限 “admin”
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
        //开放登陆接口
        filterChainDefinitionMap.put("/login", "anon");
        //其余接口一律拦截
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    /**
     * 自定义身份认证 realm;
     * <p>
     * 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
     * 否则会影响 CustomRealm类 中其他类的依赖注入
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }
}

Уведомление: Импортированный класс SecurityManager должен бытьimport org.apache.shiro.mgt.SecurityManager;Однако, если вы скопируете код, он будет импортирован по умолчанию.java.lang.SecurityManagerЗдесь также есть небольшая яма, а остальные классы относятся к классам в пакете shiro.

Метод shirFilter в основном устанавливает некоторые важные URL-адреса перехода, такие как переход без входа в систему и без разрешения; и настройка перехвата разрешений для различных URL-адресов, таких как URL-адреса, начинающиеся с /user, требуют разрешения пользователя, а URL-адреса, начинающиеся с /admin, требуют администратора. разрешения и т. д.

Фильтр перехвата разрешений

При запуске веб-приложения Широ создаст несколько полезных экземпляров фильтров по умолчанию и автоматически сделает их доступными.Эти экземпляры фильтров по умолчанию определяются классом перечисления DefaultFilter.Конечно, мы также можем настроить примеры фильтров, которые будут рассмотрены в следующих статьях.

DefaultFilter

Filter объяснять
anon Никаких параметров, открытые разрешения, можно понимать как анонимных пользователей или туристов
authc Нет параметров, требуется аутентификация
logout Нет параметра, выйдите из системы, после выполнения он перейдет прямо кshiroFilterFactoryBean.setLoginUrl();установить URL
authcBasic Параметр отсутствует, это означает httpBasic-аутентификацию
user Нет параметра, указывающего на то, что должен быть пользователь, проверка при входе не производится
ssl Нет параметров, указывающих на безопасный URL-запрос, протокол https
perms[user] Можно записать несколько параметров, что означает, что для передачи требуется одно или несколько разрешений.При наличии нескольких параметров напишите perms["user, admin"].При наличии нескольких параметров каждый параметр должен пройти для передачи.
roles[admin] Можно записать несколько параметров, что означает, что может передаваться только одна или несколько ролей. При наличии нескольких параметров напишите roles["admin, user"]. При наличии нескольких параметров каждый параметр должен пройти для передачи.
rest[user] Согласно запрошенному методу, он эквивалентен perms[пользователь:метод], где метод — это отправка, получение, удаление и т. д.
port[8081] Если запрошенный порт URL-адреса не 8081, перейдите к schema://serverName:8081?queryString, где schmal — это протокол http или https и т. д., serverName — это хост, который вы посещаете, 8081 — это порт порта, а queryString — это URL-адрес, который вы посещаете Параметр после ?

Наиболее часто используемые: anon, authc, user, roles, perms и т. д.

Уведомление: anon, authc, authcBasic, user — первая группа фильтров аутентификации, perms, port, rest, roles, ssl — вторая группа фильтров авторизации.Чтобы пройти фильтр авторизации, вы должны сначала выполнить операцию аутентификации входа (т. , сначала После завершения аутентификации вы можете перейти к поиску авторизации), чтобы перейти ко второй группе авторизаторов (например, для доступа к URL-адресу, который требует разрешения ролей, если вы не вошли в систему, вы сразу перейдете кshiroFilterFactoryBean.setLoginUrl();Установить URL)

Пользовательский класс области

Сначала нам нужно наследовать класс AuthorizingRealm, чтобы настроить нашу собственную область для наших пользовательских операций аутентификации удостоверений и полномочий. Не забудьте переопределить методы doGetAuthenticationInfo и doGetAuthorizationInfo (названия этих двух методов очень похожи, не поймите меня неправильно).

public class CustomRealm extends AuthorizingRealm {
    private UserMapper userMapper;

    @Autowired
    private void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    /**
     * 获取身份验证信息
     * Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
     *
     * @param authenticationToken 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("————身份认证方法————");
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        // 从数据库获取对应用户名密码的用户
        String password = userMapper.getPassword(token.getUsername());
        if (null == password) {
            throw new AccountException("用户名不正确");
        } else if (!password.equals(new String((char[]) token.getCredentials()))) {
            throw new AccountException("密码不正确");
        }
        return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
    }

    /**
     * 获取授权信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("————权限认证————");
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获得该用户角色
        String role = userMapper.getRole(username);
        Set<String> set = new HashSet<>();
        //需要将 role 封装到 Set 作为 info.setRoles() 的参数
        set.add(role);
        //设置该用户拥有的角色
        info.setRoles(set);
        return info;
    }
}

Два переписанных метода предназначены для реализации аутентификации личности и авторизации.В shiro есть операция входа в систему.Subject.login()метод, когда мы передаем токен, который инкапсулирует имя пользователя и пароль в качестве параметров, он будет работать с этими двумя методами (не обязательно оба метода будут введены)

Метод doGetAuthorizationInfo будет использоваться только в том случае, если требуется аутентификация авторизации, например конфигурация в предыдущем классе конфигурации.filterChainDefinitionMap.put("/admin/**", "roles[admin]");При вводе /admin вы введете метод doGetAuthorizationInfo для проверки разрешений, в то время как метод doGetAuthenticationInfo требует аутентификации (например, предыдущийSubject.login()метод) для входа

Давайте поговорим о классе UsernamePasswordToken, мы можем получить имя пользователя и пароль при входе из этого объекта (логин будет использоватьnew UsernamePasswordToken(username, password);), а получение имени пользователя или пароля имеет следующие методы

token.getUsername()  //获得用户名 String
token.getPrincipal() //获得用户名 Object 
token.getPassword()  //获得密码 char[]
token.getCredentials() //获得密码 Object

Уведомление: Многие люди обнаружат, что интерфейс UserMapper и других классов нельзя внедрить через @Autowired, и при запуске программы будет сообщено об исключении NullPointerException.Существует много причин, таких как порядок загрузки Spring в Интернете, но на самом деле существует очень важный момент, на который всем стоит обратить внимание. , класс CustomRealm настраивается в shirosecurityManager.setRealm()Он задается в методе, и многие пишут прямоsecurityManager.setRealm(new CustomRealm());, это невозможно, вы должны использовать @Bean для внедрения MyRealm, вы не можете напрямую создавать новые объекты:

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

Причина также очень проста: как и вызов службы в контроллере, все они являются SpringBeans и сами по себе не могут быть новыми.

Конечно, ту же логику можно записать и так:

    @Bean
    public SecurityManager securityManager(CustomRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(customRealm);
        return securityManager;
    }

Затем просто добавьте аннотацию, например @Component, в класс CustomRealm.

Реализация функции

Все функции этой статьи реализованы интерфейсом, возвращающим данные json.

Назначить контроллер на основе разрешений URL

游客
@RestController
@RequestMapping("/guest")
public class GuestController{
    @Autowired    
    private final ResultMap resultMap;

    @RequestMapping(value = "/enter", method = RequestMethod.GET)
    public ResultMap login() {
        return resultMap.success().message("欢迎进入,您的身份是游客");
    }

    @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
    public ResultMap submitLogin() {
        return resultMap.success().message("您拥有获得该接口的信息的权限!");
    }
}
普通登陆用户
@RestController
@RequestMapping("/user")
public class UserController{
    @Autowired    
    private final ResultMap resultMap;

    @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
    public ResultMap getMessage() {
        return resultMap.success().message("您拥有用户权限,可以获得该接口的信息!");
    }
}
管理员
@RestController
@RequestMapping("/admin")
public class AdminController {
    @Autowired    
    private final ResultMap resultMap;

    @RequestMapping(value = "/getMessage", method = RequestMethod.GET)
    public ResultMap getMessage() {
        return resultMap.success().message("您拥有管理员权限,可以获得该接口的信息!");
    }
}

Внезапно заметил, что в классе CustomRealm выбрасывается исключение AccountException, теперь создайте класс для перехвата исключения

@RestControllerAdvice
public class ExceptionController {
    private final ResultMap resultMap;

    @Autowired
    public ExceptionController(ResultMap resultMap) {
        this.resultMap = resultMap;
    }

    // 捕捉 CustomRealm 抛出的异常
    @ExceptionHandler(AccountException.class)
    public ResultMap handleShiroException(Exception ex) {
        return resultMap.fail().message(ex.getMessage());
    }
}

Конечно, есть еще LoginController для входа в систему и другой обработки.

@RestController
public class LoginController {
    @Autowired    
    private ResultMap resultMap;
    private UserMapper userMapper;

    @RequestMapping(value = "/notLogin", method = RequestMethod.GET)
    public ResultMap notLogin() {
        return resultMap.success().message("您尚未登陆!");
    }

    @RequestMapping(value = "/notRole", method = RequestMethod.GET)
    public ResultMap notRole() {
        return resultMap.success().message("您没有权限!");
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public ResultMap logout() {
        Subject subject = SecurityUtils.getSubject();
        //注销
        subject.logout();
        return resultMap.success().message("成功注销!");
    }

    /**
     * 登陆
     *
     * @param username 用户名
     * @param password 密码
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ResultMap login(String username, String password) {
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        subject.login(token);
        //根据权限,指定返回数据
        String role = userMapper.getRole(username);
        if ("user".equals(role)) {
            return resultMap.success().message("欢迎登陆");
        }
        if ("admin".equals(role)) {
            return resultMap.success().message("欢迎来到管理员页面");
        } 
        return resultMap.fail().message("权限错误!");
    }
}

контрольная работа

登陆前访问信息接口

普通用户登陆

密码错误

管理员登陆

获取信息

获取信息

注销