SpringBoot + Shiro интегрирует JWT

Spring Boot Java Spring Shiro

В этой статье вы узнаете, как интегрировать JWT (веб-токен JSON) на основе shiro + springBoot.
Если вы не знаете, как shiro интегрирует SpringBoot, вы можете сначала прочитать мою предыдущую статью.«Научи вас, Широ, интегрировать SpringBoot и избегать всевозможных ям»

Прикрепите исходный код:GitHub.com/Хоуи Юань/ это…

JWT

JSON Web Token (JWT) — очень легкая спецификация. Эта спецификация позволяет нам использовать JWT для безопасной и надежной передачи информации между пользователем и сервером.

Мы используем определенную кодировку для создания токена, добавляем в него некоторую неконфиденциальную информацию и передаем ее дальше.

Полный токен:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

В этом проекте мы оговариваем, что при каждом запросе в шапку запроса нужно включать токен, и через токен проверяется разрешение, если нет, то значит он в данный момент находится в туристическом состоянии (или логирует в интерфейс входа и т. д.)

JWTUtil

Мы используем класс инструментов JWT для создания нашего токена, Этот класс инструментов в основном имеет два метода создания токена и проверки токена.

При генерации токена укажите срок действия токенаEXPIRE_TIMEи ключ подписиSECRET, затем запишите дату и имя пользователя в токен и подпишите с помощью алгоритма подписи HS256 с ключом

Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWT.create()
   .withClaim("username", username)
   //到期时间
   .withExpiresAt(date)
   //创建一个新的JWT,并使用给定的算法进行标记
   .sign(algorithm);

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

user

роль: роль; разрешение: разрешение; запрет: статус запрета


role

У каждого пользователя есть соответствующая роль (пользователь, админ) и разрешения (обычный, вип), причем разрешение по умолчанию для роли пользователя — обычное, а разрешение по умолчанию для роли администратора — вип (разумеется, пользователь тоже может быть вип)

фильтр

В предыдущей статье мы использовали фильтр перехвата разрешений shiro по умолчанию, и из-за интеграции JWT нам нужно настроить собственный фильтр JWTFilter, который наследует BasicHttpAuthenticationFilter и переписывает некоторые исходные методы.

Фильтр состоит из трех основных шагов:

  1. Проверить, есть ли в заголовке запроса токен((HttpServletRequest) request).getHeader("Token") != null
  2. Если токен есть, выполните метод login() shiro и отправьте токен в Realm для проверки; если токена нет, текущее состояние — это туристическое состояние (или какой-либо другой интерфейс, не требующий аутентификации).
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        //判断请求的请求头是否带上 "Token"
        if (((HttpServletRequest) request).getHeader("Token") != null) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                //token 错误
                responseError(response, e.getMessage());
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JWTToken jwtToken = new JWTToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }
  1. Если в процессе проверки токена возникает ошибка, например сбой проверки токена, я расцениваю запрос как сбой аутентификации и перенаправляю на/unauthorized/**

Кроме того, я включил междоменную поддержку этого фильтра для обработки

Класс области

Это по-прежнему наш собственный Realm. Если вы не знаете об этом, вы можете сначала прочитать мою последнюю статью Широ.

  • Аутентификация
if (username == null || !JWTUtil.verify(token, username)) {
    throw new AuthenticationException("token认证失败!");
}
String password = userMapper.getPassword(username);
if (password == null) {
    throw new AuthenticationException("该用户不存在!");
}
int ban = userMapper.checkUserBanStatus(username);
if (ban == 1) {
    throw new AuthenticationException("该用户已被封号!");
}

Возьмите токен, проверьте, действителен ли токен, существует ли пользователь и его титул.

  • Авторизация аутентификации
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = userMapper.getRole(username);
//每个角色拥有默认的权限
String rolePermission = userMapper.getRolePermission(username);
//每个用户可以设置新的权限
String permission = userMapper.getPermission(username);
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
//需要将 role, permission 封装到 Set 作为 info.setRoles(), info.setStringPermissions() 的参数
roleSet.add(role);
permissionSet.add(rolePermission);
permissionSet.add(permission);
//设置该用户拥有的角色和权限
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);

Используйте имя пользователя, полученное в токене, чтобы найти роли и разрешения пользователя из базы данных и сохранить их в SimpleAuthorizationInfo.

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

Настройте наш собственный фильтр и пропустите все запросы через наш фильтр, кроме того, который мы используем для обработки неаутентифицированных запросов./unauthorized/**

@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

    // 添加自己的过滤器并且取名为jwt
    Map<String, Filter> filterMap = new HashMap<>();
    //设置我们自定义的JWT过滤器
    filterMap.put("jwt", new JWTFilter());
    factoryBean.setFilters(filterMap);
    factoryBean.setSecurityManager(securityManager);
    Map<String, String> filterRuleMap = new HashMap<>();
    // 所有请求通过我们自己的JWT Filter
    filterRuleMap.put("/**", "jwt");
    // 访问 /unauthorized/** 不通过JWTFilter
    filterRuleMap.put("/unauthorized/**", "anon");
    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;
}

Аннотация управления разрешениями @RequiresRoles, @RequiresPermissions

Эти две аннотации являются нашими основными аннотациями управления разрешениями, такими как

// 拥有 admin 角色可以访问
@RequiresRoles("admin")
// 拥有 user 或 admin 角色可以访问
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
// 拥有 vip 和 normal 权限可以访问
@RequiresPermissions(logical = Logical.AND, value = {"vip", "normal"})
// 拥有 user 或 admin 角色,且拥有 vip 权限可以访问
@GetMapping("/getVipMessage")
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
@RequiresPermissions("vip")
public ResultMap getVipMessage() {
    return resultMap.success().code(200).message("成功获得 vip 信息!");
}

Когда интерфейс, который мы написали, имеет вышеуказанные аннотации, если запрос не имеет токена или имеет токен, но аутентификация авторизации не удалась, будет сообщено об исключении UnauthenticatedException, но я централизованно обрабатываю эти исключения в классе ExceptionController.

@ExceptionHandler(ShiroException.class)
public ResultMap handle401() {
    return resultMap.fail().code(401).message("您没有权限访问!");
}

В это время, когда возникает исключение, связанное с сиро, возвращается

{
    "result": "fail",
    "code": 401,
    "message": "您没有权限访问!"
}

В дополнение к двум вышеуказанным существуют аннотации, такие как @RequiresAuthentication, @RequiresUser

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

Роли пользователей делятся на три категории: администратор-администратор, обычный пользователь-пользователь и гость-гость; разрешение администратора по умолчанию — vip, а разрешение пользователя по умолчанию — обычное. быть доступным.

Конкретную реализацию можно увидеть в исходном коде (адрес указан в начале)

авторизоваться

В интерфейсе входа нет токена. Когда пароль для входа и имя пользователя проверены правильно, токен возвращается.

@PostMapping("/login")
public ResultMap login(@RequestParam("username") String username,
                       @RequestParam("password") String password) {
    String realPassword = userMapper.getPassword(username);
    if (realPassword == null) {
        return resultMap.fail().code(401).message("用户名错误");
    } else if (!realPassword.equals(password)) {
        return resultMap.fail().code(401).message("密码错误");
    } else {
        return resultMap.success().code(200).message(JWTUtil.createToken(username));
    }
}
{
    "result": "success",
    "code": 200,
    "message": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MjUxODQyMzUsInVzZXJuYW1lIjoiaG93aWUifQ.fG5Qs739Hxy_JjTdSIx_iiwaBD43aKFQMchx9fjaCRo"
}

Обработка исключений

    // 捕捉shiro的异常
    @ExceptionHandler(ShiroException.class)
    public ResultMap handle401() {
        return resultMap.fail().code(401).message("您没有权限访问!");
    }

    // 捕捉其他所有异常
    @ExceptionHandler(Exception.class)
    public ResultMap globalException(HttpServletRequest request, Throwable ex) {
        return resultMap.fail()
                .code(getStatus(request).value())
                .message("访问出错,无法访问: " + ex.getMessage());
    }

Контроль доступа

  • UserController (доступен пользователю или администратору)
    включить интерфейс@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})

    • VIP-разрешения
      Плюс@RequiresPermissions("vip")
  • AdminController (администратор может получить доступ)
    включить интерфейс@RequiresRoles("admin")

  • GuestController (доступный для всех) не обрабатывает разрешения

Результаты теста

без токена
принести жетон
с неправильным токеном
туристы, без жетона
Доступ к непривилегированным интерфейсам (vip)
Пользователь был забанен