В этой статье вы узнаете, как интегрировать 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 и переписывает некоторые исходные методы.
Фильтр состоит из трех основных шагов:
- Проверить, есть ли в заголовке запроса токен
((HttpServletRequest) request).getHeader("Token") != null
- Если токен есть, выполните метод 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;
}
- Если в процессе проверки токена возникает ошибка, например сбой проверки токена, я расцениваю запрос как сбой аутентификации и перенаправляю на
/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")
- VIP-разрешения
-
AdminController (администратор может получить доступ)
включить интерфейс@RequiresRoles("admin")
-
GuestController (доступный для всех) не обрабатывает разрешения
Результаты теста
без токенапринести жетон
с неправильным токеном
туристы, без жетона
Доступ к непривилегированным интерфейсам (vip)
Пользователь был забанен