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

如有問題,歡迎添加個人微信討論

個人微信號

歡迎訂閱個人微信公衆號

個人公衆號

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