Shiro 使用 Token进行认证

引言

在一些老项目中,可能使用shiro进行权限认证和校验,而shiro是基于cookie/session的, 在现在前后端分离开发的场景下,前端开发人员需要在本地和后端进行调试,势必会遇到跨域的问题。 而现在随着谷歌浏览器升级,已经禁止在跨域的情况下携带cookie。

所以,基于此背景,对老项目的shiro框架进行一番改造,使得支持token的认证方式,又不影响旧代码。

在进行Shiro 改造之前,首先我们得先了解下Shiro的基本机制。这里主要涉及到4个组件:

  • AuthenticationToken 身份验证的抽象接口,该接口会返回2个信息,用户信息(Principal)和凭证信息(Credentials)
  • CredentilsMatcher 凭证校验接口,其实就是密码的校验器
  • AuthenticatingFilter 身份验证过滤器,在请求过来的时候创建AuthenticationToken对象以及执行登录操作
  • AuthorizingRealm 授权接口,在该接口会获取权限信息(doGetAuthorizationInfo)和用户信息(doGetAuthenticationInfo)

基本流程如下:

image.png

改造思路

  1. 创建一个AuthenticationToken 接口的实现类,用于存放我们的Token信息
  2. 创建一个AuthenticatingFilter的实现类,这里我们需要做3件事
    • 当前Ruequest获取token,从而创建AuthenticationToken对象
    • onAccessDenied方法,校验Token的有效性
    • 最后执行登录操作,这里的登录操作其实是用token换取用户信息,会执行AuthorizingRealmdoGetAuthenticationInfo方法
  3. 创建一个AuthorizingRealm的实现类,这里主要做3件事
    • 重写supports方法,使得支持我们自定义的Token
    • 实现doGetAuthorizationInfo方法,这里是返回用户的权限集合
    • 实现doGetAuthenticationInfo方法,这里我们根据Token获取用户信息
  4. 创建一个CredentilsMatcher接口的实现类,这里我们不对密码进行校验,直接返回true。因为当你能拿到Token,证明账号密码已经校验过。所以账号密码校验实际应该是在业务层进行校验,校验通过之后才创建Token

代码实现

  1. AuthenticationToken的实现类
public class ShiroToken implements AuthenticationToken {
    private String token;
    public ShiroToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}
复制代码
  1. AuthenticatingFilter的实现类
@Slf4j
public class TokenFilter extends AuthenticatingFilter {

    private static final String X_TOKEN = "X-Token";

    private ITokenService tokenService = null;

    /**
     * 创建Token, 支持自定义Token
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String token = this.getToken((HttpServletRequest)servletRequest);
        if(ObjectUtils.isEmpty(token)){
            log.error("token is empty");
            return null;
        }
        return new ShiroToken(token);
    }

    /**
     * 兼容跨域
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String token = this.getToken(request);
        if(ObjectUtils.isEmpty(token)){
            this.respUnLogin(request, response);
            return false;
        }

        // 校验Token的有效性
        if(tokenService == null){
            tokenService = SpringContext.getBean(ITokenService.class);
        }

        if(!tokenService.check(token)){
            this.respUnLogin(request, response);
        }

        // 根据token获取用户信息,会执行 TokenRealm#doGetAuthenticationInfo 方法
        return executeLogin(servletRequest, servletResponse);
    }

    private void respUnLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        Response resp = new Response(BusinessCodeEnum.USER_UN_LOGIN.getCode(), BusinessCodeEnum.USER_UN_LOGIN.getMsg());
        response.getWriter().print(JSONUtil.toJsonStr(resp));
    }

    /**
     * 获取token
     * 优先从header获取
     * 如果没有,则从parameter获取
     * @param request request
     * @return token
     */
    private String getToken(HttpServletRequest request){
        String token = request.getHeader(X_TOKEN);
        if(ObjectUtils.isEmpty(token)){
            token = request.getParameter(X_TOKEN);
        }
        return token;
    }
}
复制代码

这里需要特别注意TokenFilterFilter的实现类,并不在Spring的容器中管理,所以无法通过@Autowire等注解进行注入,只能通过构造函数或者在使用的时候通过Spring的上下文中获取。建议通过Spring的上下文中获取,通过构造器注入可能采坑。

  1. AuthorizingRealm的实现类
@Slf4j
public class TokenRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private ITokenService tokenService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ShiroToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("user do authorization: {}", principalCollection);
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("user do authentication: {}", authenticationToken);
        ShiroToken token = (ShiroToken)authenticationToken;
        UserInfo userInfo = tokenService.getUserInfo(token.getCredentials().toString());
        if(userInfo == null){
            throw BusinessCodeEnum.TOKEN_INVALID.getException();
        }
        return new SimpleAuthenticationInfo(userInfo.getUsername(), userInfo.getPassword(), userInfo.getNickName());
    }
}
复制代码
  1. CredentilsMatcher的实现类
public class TokenCredentialsMatcher implements CredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        return token instanceof ShiroToken;
    }
}
复制代码

最后

通过以上的简单改造,我们就可以实现基于Token的认证方式又不需要改动Shiro框架的其他功能了。

完整源码地址:shiro-to-token

如有问题,欢迎添加个人微信讨论

个人微信号

欢迎订阅个人微信公众号

个人公众号

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章