想要实现基于SpringBoot+Shiro+Vue的前后端分离技术,网上教程还是不少的,在实现成功后,多问了个问题,就有了这篇文章。问题如标题,如果不使用Shiro提供的Filter模式会怎么样:

    前后端分离,如果没用RESTful架构,使用JSON rpc等类似协议,
    其实是不需要Shiro基于URL路径去判断用户权限,只需要使用注解来
    标识函数权限即可,如:
    //统一的接口入口
    @RequestMapping(value = "/api",produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String api(HttpServletRequest request) {
        //调用不同接口
    }
    //需要用户已登录
    @RequiresAuthentication
    public Result getUserInfo(JSONObject json){
        //do something
    }

       所以,我注释掉了Shiro的Filter注册部分,然后理所当然的认为直接就可用了,也理所当然的被打脸了。原因是并没有真正了解Shiro的设计模式与其在Shiro-web中所做的事。

一、总结:Subject其实是绑定线程的

       其实如果多想想,这样设计是符合Shiro定位的。Shiro本意就是不依赖任何容器与环境,可以在任意的Java环境中提供用户权限认证,如果Shiro是绑定进程的,独立App可能还没问题,因为只需要提供给一个用户使用,但服务器模式等需要为多用户提供用户鉴权的,就没办法实现了。
       基于这个前提,Shiro-web的Filter,利用Filter的机制,在所有请求真实处理前,根据SessionID获取Session(SessionID获取方式可自定义,默认使用Cookie,不需要干预;也可以修改为Header、Query等等),构造一个Subject绑定到线程中,后续的权限相关处理,就都是针对这个用户的了。在该线程中,任意位置都可以直接调用:Subject subject = SecurityUtils.getSubject();来获取用户的Subject。
       Request处理完后,还需要对线程进行处理,将相关数据与线程解绑。
       关键代码:

//创建Subject
//org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {
    //....
    //封装一下Request与Response
    final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
    final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
    //生成Subject,生成时会获取SessionID,如果存在,根据Session生成Subject
    final Subject subject = createSubject(request, response);

    //用Subject提供的接口来绑定Subject到线程,并执行后续操作
    subject.execute(new Callable() {
        public Object call() throws Exception {
            updateSessionLastAccessTime(request, response);
            executeChain(request, response, chain);
            return null;
        }
    });
    //....
}

//其中,使用subject.execute来执行代码,保证线程与数据的绑定与解绑
//org.apache.shiro.subject.support.SubjectCallable#call
public V call() throws Exception {
    try {
        threadState.bind();
        return doCall(this.callable);
    } finally {
        threadState.restore();
    }
}

//获取SessionID
/**
getSessionId:273, DefaultWebSessionManager (org.apache.shiro.web.session.mgt)
...
resolveSession:446, DefaultSecurityManager (org.apache.shiro.mgt)
...
buildSubject:845, Subject$Builder (org.apache.shiro.subject)
*/
//org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getSessionId(org.apache.shiro.session.mgt.SessionKey)
@Override
public Serializable getSessionId(SessionKey key) {
    Serializable id = super.getSessionId(key);
    if (id == null && WebUtils.isWeb(key)) {
        ServletRequest request = WebUtils.getRequest(key);
        ServletResponse response = WebUtils.getResponse(key);
        id = getSessionId(request, response);
    }
    return id;
}

       题外话:Shiro-web中的Session是直接利用了HttpSession,对其做了一层封装。Shiro默认也提供了一种Session——SimpleSession。

二、手写实现简单验证

       先约定一下接口:

  • 接口统一使用"/api",POST方式传入JSON数据,使用cmd来区分实际接口,如:{"cmd":"logout"}
  • SessionID前端使用X-TOKEN头传入

       通用配置就不列出来了,Shiro配置相当简化,只要提供一个Realm的配置即可:

1. Shiro配置

@Configuration
@Import({ShiroBeanConfiguration.class,
        ShiroConfiguration.class})
public class ShiroConfig {

    /**
     * 自定义安全域,用户验证、权限等数据在此提供
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    /**
     * 凭证匹配器,这里的hash算法用来将前台传入的数据做处理
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }
    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

//绑定SecurityManager
@Resource
private SecurityManager securityManager;

@PostConstruct
private void initStaticSecurityManager() {
    SecurityUtils.setSecurityManager(securityManager);
}

2.使用

       用的时候也很简单

  • /api接口中,绑定Subject到进程,再调用后续服务
//首先解绑验证数据,保证Request开始的纯洁性
ThreadContext.unbindSubject();
if(StringUtils.isNotEmpty(request.getHeader("X-TOKEN"))){
    //指定SessionID创建Subject
    Subject subject = (new Subject.Builder()).sessionId(request.getHeader("X-TOKEN")).buildSubject();
    if(subject != null){
        //验证数据绑定线程数据
        ThreadContext.bind(subject);
    }
}
  • 用户登录
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
  • 权限控制,直接对相关函数做注解
//需要登录
@RequiresAuthentication
//需要权限
@RequiresPermissions("user:add")
//需要角色
@RequiresRoles("admin")

三、再总结

       这当然只是自己的瞎折腾,web项目还是建议直接使用Shiro-web来实现,相当简洁优雅。不过生命在于折腾,对于Shiro的设计模式、web验证等又多了许多新的认识。

Logo

前往低代码交流专区

更多推荐