欢迎光临散文网 会员登陆 & 注册

Shiro 使用 Token进行认证

2023-02-06 22:25 作者:Anyin灬  | 我要投稿

引言

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

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

Shiro 基本机制

在进行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框架的其他功能了。

完整源码地址:https://gitee.com/anyin/shiro-to-token


Shiro 使用 Token进行认证的评论 (共 条)

分享到微博请遵守国家法律