想要做一个Shiro + JWT的整合,用 JWT 生成token,使用shiro进行身份认证和权限分配。
Shiro进行身份认证的流程大致说一下(自己瞎画的):
可以看到,我们需要实现的就是何时调用login方法,怎么样编写自定义的Realm来具体实现我们的认证逻辑。 因为项目是JWT生成的token,所以为了能够满足要求,这里就实现了AuthenticationToken这个借口,这样在login的时候就可以把自己的token传到这个流程中去,再通过自己的realm进行token的解析认证。
为了实现我们自己的Realm,我们看官网的介绍,我们可以继承AuthorizingRealm,如下图:
可以发现他继承了CachingRealm,具有缓存的功能,而且还具有权限验证的功能,所以选择了它。
贴上这个Realm的代码:
public class TokenRealm extends AuthorizingRealm {
@Autowired
UserLoginServiceImpl userLoginService;
@Autowired
UserRoleServiceImpl userRoleService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限验证
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
UserLogin userLogin = (UserLogin) principals.asList().get(0);
List<String> permissionsByRole = userRoleService.getPermissionsByRole(userLogin.getUserRole());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole(userLogin.getUserRole());
authorizationInfo.addStringPermissions(permissionsByRole);
return authorizationInfo;
}
/**
* 身份验证码
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwtToken = (String) token.getCredentials();
//获得userId 与数据库进行比对,检查是否存在该项
Long userId = JWTUtil.getUserId(jwtToken);
if (userId == null || jwtToken == null) {
throw new ShiroException(ResultCode.TOKEN_FAILED.getCode(),
ResultCode.TOKEN_FAILED.getMessage());
}
//去数据库中查找
UserLogin userInfo = (UserLogin) userLoginService.checkUserById(userId).getData();
if (userInfo == null) {
throw new ShiroException(ResultCode.USERLOGIN_NOT_EXISTS.getCode(),
ResultCode.USERLOGIN_NOT_EXISTS.getMessage());
}
//跨用户检测
Subject subject = SecurityUtils.getSubject();
Object userIdFromRequest = subject.getSession().getAttribute("userId");
if (!userInfo.getUserId().toString().equals(userIdFromRequest.toString())) {
throw new ShiroException(ResultCode.ILLEGL_OPREATE.getCode(),
ResultCode.ILLEGL_OPREATE.getMessage());
}
//校验token
if (!JWTUtil.verifyToken((String) token.getCredentials(), userInfo)) {
throw new ShiroException(ResultCode.TOKEN_FAILED.getCode(),
ResultCode.TOKEN_FAILED.getMessage());
}
return new SimpleAuthenticationInfo(userInfo, token.getCredentials(), getName());
}
}
这里可以看到我们在几种情况下抛出了异常,官方给我们提供了常见异常:
AuthencationException: AuthenticationException
异常是Shiro在登录认证过程中,认证失败需要抛出的异常。AuthenticationException包含以下子类:
CredentitalsException 凭证异常
IncorrectCredentialsException 不正确的凭证
ExpiredCredentialsException 凭证过期AccountException 账号异常
ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
UnknownAccountException 未知的账号
ExcessiveAttemptsException 认证次数超过限制
DisabledAccountException 禁用的账号
LockedAccountException 账号被锁定3.UnsupportedTokenException 使用了不支持的Token
一。项目遇到的问题:
在实际做项目的时候遇到了一个问题,我们抛出的异常最终都没有成功被抛出,反而抛出的是一个多Realm没有通过认证的异常,初步判断是我们自己想要在Realm抛出的异常没有抛出,所以看源码发现在认证器做认证的时候:
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
/**
* 修改后
* 由于在多realm认证的时候会将异常覆盖掉,
* 这里在每次realm认证结束后,将异常抛出。
*/
if (t instanceof UnknownAccountException) {
throw new UnknownAccountException();
} else if (t instanceof IncorrectCredentialsException) {
throw new IncorrectCredentialsException();
} else if (t instanceof ShiroException) {
throw new ShiroException(((ShiroException) t).getCode(), t.getMessage());
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
并没有抛出异常,而是进行接下来其他realm的认证,最终就只能抛出一个普通的认证失败的异常,覆盖了前面的我们自己抛出的异常,修改后重写该方法,我们对异常进行一个判断,对异常进行一个再抛出。而不进行其他realm的认证了,这样就解决了。
二.
解决了上一个问题,随之而来的是另一个问题,抛出的异常是在Filter期间抛出的异常,所以我们无法直接通过controller层设置的@ControllerAdvice+@ExceptionHandler(value = BaseException.class)进行异常处理,所以我们需要自己定义一个全局的Filter异常处理器。
参考博客上其他人的方法:实现思路是实现一个Filter并设置启动顺序为所有Filter的最前面,,也就是setOrder(1),这样的话,当在其他Filter发生异常的时候可以通过调用链捕获到发生的异常,然后将请求转发到我们制定一个errorController来通过HttpServletRequest接收异常,既然在controller得到异常信息,我们也就可以使用之前的全局异常处理器来处理异常了。
贴一下Filter代码:
@Slf4j
public class ExceptionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("ExceptionFilter init . . . ");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (ServletException e) {
request.setAttribute("authenticateException", e);
request.getRequestDispatcher("/rethrow").forward(request, response);
}
}
@Override
public void destroy() {
log.info("ExceptionFilter destroy . . . ");
}
}
贴下errorController代码:
@Controller
public class ErrorController {
@RequestMapping("/rethrow")
@ResponseBody
public ResultData throwShiroException(HttpServletRequest request) throws BaseException {
ServletException exception = (ServletException) request.getAttribute("authenticateException");
throw (ShiroException) exception.getCause();
}
}
由于抛出的异常都是AuthenticationException的子类,我自己定义了一个ShiroException继承自前者,并且设置了状态码和异常信息,进而返回给前端便于显示。
@ExceptionHandler(value = ShiroException.class)
@ResponseBody
public ResultData shiroExceptionHandler(ShiroException e) {
e.printStackTrace();
return ResultData.builder().code(e.getCode()).message(e.getMessage()).build();
}
**
暂时,就这些吧!
**