写在前面
本文的出现表示不再进行 Spring Security Oauth
实现的研究了,原因是原开源项目已经被废弃了不再更新了,而且 Oauth
实现的内容有些奇怪,新的项目 spring-authorization-server 目前才发布到 0.1.0
,默认只提供了基于内存的实现,个人认为还不是很完善,不适合用到项目中。而且 Spring Security
的 Oauth
流程都实现了,要修改还得从新研究 spring-authorization-server
的实现逻辑,然后进行修改定制,太耗费精力了,不如使用 Shiro
自己实现 Oauth
的逻辑。
但是本文和 Oauth
无关,单纯是 Shiro + Springboot + JWT 的整合。
本文使用的依赖如下:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
还有一点要说明的是,Shiro
有针对 SpringBoot
的默认配置,详细配置见官网:
- 与
Spring
集成:http://shiro.apache.org/spring-framework.html - 与
SpringBoot
集成:http://shiro.apache.org/spring-boot.html
但是在本文中并没有使用 SpringBoot
集成方式,也没有按官网的教程导入默认配置,因为 Shiro
要运行起来配置相对较于简单,需要配置的并不多,而且从本人的了解来看,Shiro
适合基于 Session
进行登录状态存储的模式,使用 Jwt
将状态存储在了 Token
里,这种模式对配置的逻辑需要做部分更改,而且还有些坑,所以没有基于默认配置进行配置,需要注意的地方以下会有说明。
记录贴,刚接触 Shiro
,内容如有错误,望大佬指正。
1. 实现源码
示例项目已经上传到 Github 直接可用:https://github.com/nineya/framework-study/tree/v0.1.0/shiro-study
项目结构:
1.1 SpringBoot 程序入口
package com.nineya.shiro;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author 殇雪话诀别
* 2021/2/15
* 程序入口
*/
@SpringBootApplication
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class);
}
}
1.2 实体类
简单自定义用户、角色和权限实体类,一个用户可以包含多个角色,一个角色可以包含多个权限。
package com.nineya.shiro.entity;
/**
* @author 殇雪话诀别
* 2021/2/15
* 权限
*/
public class Permissions {
private long id;
private String permissionsName;
public Permissions() {
}
public Permissions(long id, String permissionsName) {
this.id = id;
this.permissionsName = permissionsName;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getPermissionsName() {
return permissionsName;
}
public void setPermissionsName(String permissionsName) {
this.permissionsName = permissionsName;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Permissions{");
sb.append("id=").append(id);
sb.append(", permissionsName='").append(permissionsName).append('\'');
sb.append('}');
return sb.toString();
}
}
package com.nineya.shiro.entity;
import java.util.Set;
/**
* @author 殇雪话诀别
* 2021/2/15
* 角色,包含权限集合
*/
public class Role {
private long id;
private String roleName;
/**
* 角色拥有的权限集合
*/
private Set<Permissions> permissions;
public Role() {
}
public Role(long id, String roleName, Set<Permissions> permissions) {
this.id = id;
this.roleName = roleName;
this.permissions = permissions;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<Permissions> getPermissions() {
return permissions;
}
public void setPermissions(Set<Permissions> permissions) {
this.permissions = permissions;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Role{");
sb.append("id=").append(id);
sb.append(", roleName='").append(roleName).append('\'');
sb.append(", permissions=").append(permissions);
sb.append('}');
return sb.toString();
}
}
package com.nineya.shiro.entity;
import java.util.Set;
/**
* @author 殇雪话诀别
* 2021/2/15
* 用户,包含角色集合
*/
public class User {
private long uid;
private String userName;
private String password;
/**
* 用户对应的角色
*/
private Set<Role> roles;
public User() {
}
public User(long uid, String userName, String password, Set<Role> roles) {
this.uid = uid;
this.userName = userName;
this.password = password;
this.roles = roles;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("User{");
sb.append("uid=").append(uid);
sb.append(", userName='").append(userName).append('\'');
sb.append(", password='").append(password).append('\'');
sb.append(", roles=").append(roles);
sb.append('}');
return sb.toString();
}
}
1.3 服务层
创建 Login
服务层实现,能够取读取用户信息进行用户登录操作,在实际使用时此处修改为;连接数据库获取数据。
package com.nineya.shiro.service;
import com.nineya.shiro.entity.User;
/**
* @author 殇雪话诀别
* 2021/2/15
* 用户登录服务接口
*/
public interface LoginService {
/**
* 通过用户名取得用户
*
* @param name 用户名
* @return
*/
User getUserByName(String name);
}
package com.nineya.shiro.service.impl;
import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @author 殇雪话诀别
* 2021/2/15
* 用户登录服务,使用Map<用户名, 用户信息> 的格式存储用户信息进行查询
* 在实际使用中将此处修改为连接数据库查询即可
*/
@Service
public class LoginServiceImpl implements LoginService {
private final Map<String, User> users = new HashMap<>();
public LoginServiceImpl() {
// 定义三个权限
Permissions permissions1 = new Permissions(1, "create");
Permissions permissions2 = new Permissions(2, "delete");
Permissions permissions3 = new Permissions(3, "select");
// 定义两个角色
Role role1 = new Role(1, "read", new HashSet<Permissions>(){{add(permissions3);}});
Role role2 = new Role(1, "write", new HashSet<Permissions>(){{add(permissions1);add(permissions2);}});
// 定义三个用户分别对应两个角色
users.put("observe", new User(1, "observe", "123456", Collections.singleton(role1)));
users.put("admin", new User(1, "admin", "123456", Collections.singleton(role2)));
users.put("user", new User(1, "user", "123456", new HashSet<Role>(){{add(role1); add(role2);}}));
}
@Override
public User getUserByName(String name) {
return users.get(name);
}
}
1.4 Token 工具类
token
工具类负责 Jwt
的创建和解析,在本文中将 username
加入到负载,实际使用中可以将角色等信息一起加入。但是不应该将大量数据和隐式数据加入到 jwt
中,因为 jwt
没有加密,大量的数据会导致 token
过长,加重网络负载。
package com.nineya.shiro.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
/**
* jwt处理类
*
* @author 殇雪话诀别
* 2020/11/29
*/
@Component
public class UserTokenUtil {
/**
* jwt 加密算法
*/
private Algorithm algorithm;
private JWTVerifier verifier;
public UserTokenUtil() {
algorithm = Algorithm.HMAC256("secret");
verifier = JWT.require(algorithm).build();
}
/**
* 创建用户token,并将token创建时间存入
*
* @param username 用户名称
* @return token字符串
*/
public String createToken(String username) {
Date expireTime = new Date(System.currentTimeMillis() + 60 * 60 * 1000);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(expireTime)
.sign(algorithm);
}
/**
* 校验token合法性
*
* @param token
* @return
*/
public DecodedJWT verifyToken(String token) {
try {
return verifier.verify(token);
} catch (Exception e) {
throw new TokenExpiredException("token 解析失败");
}
}
/**
* 取得用户名
*
* @param token
* @return
*/
public String getUserName(String token) {
DecodedJWT jwt = verifyToken(token);
return jwt.getClaim("username").asString();
}
}
1.5 控制器(开始涉及Shiro)
添加 login
登录接口实现,添加几个简单接口作为后续的调用示例,此处开始设计上面所述的 jwt
逻辑不同的问题了。
package com.nineya.shiro.controller;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
/**
* @author 殇雪话诀别
* 2021/2/15
*/
@RestController
public class LoginController {
@Resource
private LoginService loginService;
@Resource
private UserTokenUtil tokenUtil;
/**
* 使用 jwt 进行登录时此处逻辑将有些不同
* 如果没有使用Token,用户将在此方法中通过 subject.login(usernamePasswordToken) 进行登录。
* 使用 jwt 时,将不再使用 session 存储登录状态,subject.login(usernamePasswordToken) 逻辑将在 Filter 解析 token 时进行,并且
* 每次请求都需要进行 token 解析和登录操作。
* 也就是说认证、授权两个步骤,原本只要登录时进行认证,每次请求进行授权,使用 jwt 后每次请求都需要记性jwt解析、认证和授权三个步骤。
* @param userName 用户名
* @param password 密码
* @return
*/
@GetMapping("/login")
public String login(@RequestParam("userName") String userName, @RequestParam("password") String password) {
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
return "请输入用户名和密码!";
}
User user = loginService.getUserByName(userName);
if (!user.getPassword().equals(password)) {
return "密码不正确!";
}
return tokenUtil.createToken(userName);
}
// 这是没有使用 jwt 时,基于 session 的实现方式
// @GetMapping("/login")
// public String login(User user) {
// if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
// return "请输入用户名和密码!";
// }
// //用户认证信息
// Subject subject = SecurityUtils.getSubject();
// UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
// user.getUserName(),
// user.getPassword()
// );
// try {
// //进行验证,这里可以捕获异常,然后返回对应信息
// subject.login(usernamePasswordToken);
subject.checkRole("admin");
subject.checkPermissions("query", "add");
// } catch (UnknownAccountException e) {
// return "用户名不存在!";
// } catch (AuthenticationException e) {
// return "账号或密码错误!";
// } catch (AuthorizationException e) {
// return "没有权限";
// }
// return "login success";
// }
/**
* 允许角色为 read 且为 write 用户访问
* @return
*/
@RequiresRoles({"read", "write"})
@GetMapping("/admin")
public String admin() {
return "admin";
}
/**
* 允许拥有 select 权限的用户访问
* @return
*/
@RequiresPermissions("select")
@GetMapping("/select")
public String select() {
return "select";
}
/**
* 允许拥有 create 权限的用户访问
* @return
*/
@RequiresPermissions("create")
@GetMapping("/create")
public String create() {
return "create";
}
}
package com.nineya.shiro.controller;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author 殇雪话诀别
* 2021/2/15
* 对未通过权限认证的部分异常进行异常处理
*/
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler
@ResponseBody
public String ErrorHandler(AuthorizationException e) {
return "没有通过权限验证!\n" + e.getMessage();
}
}
1.6 自定义 JWT 过滤器
检查请求头是否有 Authorization
字段,如果包含则进行 token
解析和登录操作。
package com.nineya.shiro.filter;
import com.nineya.shiro.controller.ExceptionController;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* @author 殇雪话诀别
* 2021/2/15
*/
public class TokenFilter extends BasicHttpAuthenticationFilter {
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(AUTHORIZATION_HEADER);
return authorization != null;
}
/**
* 该方法返回值表示是否跳过认证,这里如果返回 true,则不会再走一遍认证流程
* 如果返回 false,则会执行 isAccessAllowed 方法,再执行isLoginAttempt方法,如果为false继续执行executeLogin
* 方法,如果没有执行executeLogin或者执行结果也是false,则将执行sendChallenge方法,表示认证失败。
* 返回 true 时,则必须在postHandle配置退出登录,这个方法将在执行完业务逻辑后执行,否则将导致下次没有携带token时直接使用上次的登录
* 结果,从而非法访问接口。
* 默认返回 false 时,isLoginAttempt这些方法将重复调用,所以不建议。如果要返回 false,建议复写 sendChallenge 方法,因为其响应内容为空。
*
* 也可以将 isAccessAllowed 作为纯判断是否需要认证,或者不复写该方法,本文不提供实现,如果有问题欢迎留言
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
return executeLogin(request, response);
} catch (Exception e) {
//token 错误
e.printStackTrace();
return false;
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 用户登录
* @param request
* @param response
* @return
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
BearerToken jwtToken = new BearerToken(token, request.getRemoteAddr());
getSubject(request, response).login(jwtToken);
return true;
}
/**
* 该方法将在过滤器执行完成后执行
* 当isAccessAllowed默认为true时必须实现该方法
* 在执行完请求后执行退出登录逻辑,否则下次请求时没有携带将可以直接访问接口,无须重新登录
* @param request
* @param response
* @throws Exception
*/
@Override
protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
SecurityUtils.getSubject().logout();
}
/**
* 当 isAccessAllowed 可能返回false时需要复写该接口,否则默认将返回空白界面
* @param request
* @param response
* @return
*/
@Override
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
super.sendChallenge(request, response);
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setContentType("application/json; charset=utf-8");
try {
response.getWriter().write(
JSONObject.toJSONString(ResponseResult.failure(StatusCode.UNAUTHORIZED, "登录状态已失效!").toMap()));
} catch (IOException e) {
throw new UnauthenticatedException("登录状态已失效!");
}
return false;
}
/**
* 登录异常处理类,该方法将捕获所有在本过滤器中遇到的异常,默认将会进行登录重试,重试失败后执行 afterCompletion 方法进行善后处理,
* 后将异常封装为 ServletException。
* 建议复写本方法 jwt 登录失败,重试也是无果的。
* @param request
* @param response
* @param existing 异常信息,如果为 null表示没有异常,不需要进行处理
* @throws ServletException
* @throws IOException
*/
@Override
protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException {
// 不管什么异常,全部抛出登录失败就是了
if (existing != null) {
sendChallenge(request, response);
}
}
}
1.7 自定义 Realm
Shiro
默认提供了 JdbcRealm
、IniRealm
和 DefaultLdapRealm
等实现,但是并不是很符合我所需要的场景,也不能实现 jwt
的解析,所以采用继承 AuthorizingRealm
自定义 Realm
类,复写 doGetAuthorizationInfo
(授权)和 doGetAuthenticationInfo
(认证)两个接口。
此处其实有坑的,而且逻辑并不是很适合 jwt
的实现,但是没有好的方案进行修改,后续将详细说明。
-
此处只是示例,在授权和认证两次的
loginService.getUserByName(name)
操作均可以想办法避免。 -
可以在认证步骤解析
JWT
为中间对象,然后将中间对象作为Principal
,能够避免重复解析JWT
带来性能浪费。
package com.nineya.shiro.config;
import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.catalina.realm.AuthenticatedUserRealm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.stream.Collectors;
/**
* 自定义 realm
*
* @author 殇雪话诀别
* 2021/2/15
*/
public class StudyRealm extends AuthorizingRealm {
@Resource
private LoginService loginService;
@Resource
private UserTokenUtil tokenUtil;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof BearerToken;
}
/**
* 授权,在认证之后执行
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = (String) principals.getPrimaryPrincipal();
String name = tokenUtil.getUserName(token);
User user = loginService.getUserByName(name);
// 添加角色和权限
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
// 添加角色
simpleAuthenticationInfo.addRole(role.getRoleName());
// 添加权限
simpleAuthenticationInfo.addStringPermissions(role.getPermissions().stream()
.map(Permissions::getPermissionsName).collect(Collectors.toSet()));
}
return simpleAuthenticationInfo;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if (StringUtils.isEmpty(token.getPrincipal())) {
return null;
}
String name = tokenUtil.getUserName((String) token.getPrincipal());
User user = loginService.getUserByName(name);
if (user == null) {
return null;
}
// 第一个参数是主体,将会在授权时封装成PrincipalCollection.getPrimaryPrincipal()进行使用,所以必须将jwt内容传回
// 第二个参数是认证信息,即密码,为后面验证可以通过,需要和token中的内容一样
// 第三个参数是领域名称
return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), user.getUserName());
}
}
1.8 Shiro 配置
package com.nineya.shiro.config;
import com.nineya.shiro.filter.TokenFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* @author 殇雪话诀别
* 2021/2/15
* 配置类
*/
@Configuration
public class ShiroConfiguration {
/**
* 配置代理,没有配置将会导致注解不生效
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* 配置代理,没有配置将会导致注解不生效
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 将自己的验证方式加入容器
* @return
*/
@Bean
public Realm studyRealm() {
StudyRealm studyRealm = new StudyRealm();
return studyRealm;
}
/**
* 不应该将过滤器的实现注册为bean,否则会导致Filter过滤器顺序混乱,导致抛出异常
* 如果一定要注册为 Bean,可以使用 Order 指定优先级,还未尝试过
* @return
*/
public TokenFilter tokenFilter() {
return new TokenFilter();
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
// 使用我们自己创建的jwt过滤器名称
map.put("/**", "jwt");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/select");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
shiroFilterFactoryBean.setFilters(new HashMap<String, Filter>(){{put("jwt", tokenFilter());}});
return shiroFilterFactoryBean;
}
/**
* 权限管理,配置主要是Realm的管理认证,同时可以配置缓存管理等
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
//realm管理
webSecurityManager.setRealm(studyRealm());
return webSecurityManager;
}
}
2. 为什么说 Shiro 对 JWT支持不是很好?
本文一开始就说了 Shiro
对 Jwt
模式的支持不是很好,全部都是本人个人的理解,如果内容有误请留言提问。
2.1 认证/授权模式藏BUG
- 对性能的影响
Shiro
整体都是围绕着认证/授权模式展开的,该模式适用于 有状态
(即服务端存储登录信息)的系统,在登录时进行 认证
,一旦登录完成后就不再需要进行认证步骤,直接根据权限配置进行认证。而 JWT
是 无状态
的实现方式,所有的登录信息都存储在 Token
里,在过滤器中每次都需要根据 token
信息执行 getSubject(request, response).login(jwtToken)
方法,然后都会走一次 realm
的 doGetAuthenticationInfo
方法进行认证。
对于 JWT
来说,这个授权和认证步骤实际上是可以合并在一起作为授权步骤,登录账号密码校验才是认证步骤,但是不能在登录时调用 getSubject(request, response).login(jwtToken)
进行登录,必须在过滤器中根据 token
进行调用登录,这导致了性能的浪费,可以在认证步骤解析 JWT
为中间对象,然后将中间对象作为 Principal
,能够避免重复解析 JWT
带来性能浪费,但是 Shiro
重复走认证流程的性能浪费不可避免。
- cache 藏 BUG
由于 JWT
的 Token
具有时效性,而 Token
又是作为认证步骤的凭证,如果开启了缓存,缓存将根据 Token
进行存储,导致后续对于该 Token
不会再进行认证流程,将会导致失效的 Token
还可以访问系统。
恶心的是 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
方法无法复写的,所以只能关闭缓存功能才能解决这个问题,或者手动实现这些内容。如果关闭缓存的话,授权步骤又是同样的缓存逻辑,授权步骤的缓存也将被一并关闭,对性能也会造成影响。
综上,Shiro
对 JWT
的支持并不是很好,但是也不是没办法解决,手动复写这些类就好,只是将会比较繁琐。