项目总结一(Shiro+JWT)

想要做一个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包含以下子类:

  1. CredentitalsException 凭证异常
    IncorrectCredentialsException 不正确的凭证
    ExpiredCredentialsException 凭证过期

  2. 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();
    }

**

暂时,就这些吧!

**

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值