美文网首页
Shiro 整合 JWT 实现无状态 Web 服务

Shiro 整合 JWT 实现无状态 Web 服务

作者: hemiao3000 | 来源:发表于2022-02-11 07:40 被阅读0次

Subject 工厂(非必须)

无状态的 Web 服务(RESTful),意味着我们不会使用 Shiro 的 Session 功能,更用不上 SessionDAO 。因此,严谨的做法可以在 Security Manager 的配置中将这两个功能关闭掉。

当然,你不关闭也可以,不使用它们即可。

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不创建 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

通过调用 context.setSessionCreationEnabled(false) 表示不创建会话;如果之后调用 subject.getSession() 将抛出 DisabledSessionException 异常。

在之前的『页面跳转式』的项目中,Shiro 将 Subject 的信息存入了 Session 中,并且每个 Subject 是有『状态』的概念的<small>(已经登录、未登录)</small>。

JwtAuthenticationFilter

类似于 FormAuthenticationFilter,但是根据当前请求上下文信息每次请求时都要登录的认证过滤器。

AccessControlFilter 有两个方法:isAccessAllowed 方法和 onAccessDenied 方法:

  • 如果 isAccessAllowed() 返回 true ,Shiro 将放过请求,允许访问 URL<small>(这时 Shiro 不考虑 onAccessDenied() 方法)</small>,接下来流程就会走到 Controller ;

  • 如果 isAccessAllowed() 返回 false,那么 Shiro 再来考虑 onAccessDenied() 方法。onAccessDenied() 方法返回 true,则表示请求继续,返回 fase,则表示认证不通过,需要返回给用户登陆页面<small>(或表示错误信息的 JSON 信息)</small>。

@Slf4j
public class JwtAuthenticationFilter extends AccessControlFilter {

    /**
     * 该方法决定 JwtFilter 是否放行请求。
     * 该方法返回 true,那么当前请求将走向 Controller 。
     * 该方法返回 false,那么当前请求有可能走向下一个 Filter,也有可能就此打回。这取决于下面的 onAccessDenied 方法的返回值。
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String jwt = httpServletRequest.getHeader("x-jwt-token");
        log.info("jwt: {}", jwt);
        if (!StringUtils.hasText(jwt)) {
            log.info("进入 onAccessDenied 方法");
            return false;
        }

        JwtToken token = new JwtToken(jwt);
        try {
            subject.login(token);
            log.info("jwt 合法,进入Controller。");
            return true;
        } catch (RuntimeException e) {
            e.printStackTrace();
            log.info("进入 onAccessDenied 方法");
            return false;
        }
    }

    /**
     * 该方法决定当前请求在不符合当前 Filter 的同行标准的前提下,接下来是走向下一个 Filter ,还是就此打回。
     * 该方法返回 true,那么当前请求将走向下一个 Filter 。
     * 该方法返回 false,那么当前请求将不再触发下一个 Filter 的执行,就此打回。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 当前请求到此为止了:控制跳转显示登录页面,或显示 JSON 串
        onAuthenticatedFailure(request, response);
        return false;
    }

    /**
     * 私有方法,onAccessDenied 返回 false 时调用,控制重定向登录页面,或返回 JSON 字符串。
     */
    private void onAuthenticatedFailure(ServletRequest request, ServletResponse response) throws IOException {

        // 跳转页面
        saveRequestAndRedirectToLogin(request, response);

        /*
        // 返回 JSON 串
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("unlogin");
        */
}

和客户端约定好,要求客户端发送的请求,在请求头中传递 jwt-token-string ,然后生成 JwtToken 对象,再触发 Security Manager 进行 subject 的认证。

JwtToken

public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken (String  token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

用户身份和凭证都是 token 。

JwtRealm

用于认证的 Realm 。

public class JwtRealm extends AuthorizingRealm {

    @Autowired
    private JwtUtil jwtUtil;

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

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        JwtToken jwtToken = (JwtToken) token;

        String jwt = jwtToken.getJwt();
        log.info("待校验的 jwt 为 {}", jwt);

        SimpleAuthenticationInfo info = null;

        try {
            jwtUtil.isVerify(jwt);
            log.info("realm 认为 jwt 合法");
            info = new SimpleAuthenticationInfo(jwt, jwt, getName());
            return info;
        } catch (RuntimeException e) {
            log.info("relm 认为 jwt 非法");
            e.printStackTrace();
        }

        return null;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 逻辑上,doGetAuthorizationInfo 方法与 JwtAuthenticationFilter 的工作无关。
    }
}

基于 starter 整合 JWT

@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm realm() {
        JwtRealm realm = new JwtRealm();
        return realm;
    }

    /**
     * 由于使用自定义 Realm,所以要显式配置 SecurityManager 。
     */
    @Bean
    public DefaultWebSecurityManager securityManager(JwtRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(realm);

        /**
         * 非必须。以下配置与 Shiro 整合 JWT 的核心功能无关。
         */
        // 关闭 ShiroDAO 功能。
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        // 禁止 Subject 的 getSession 方法
//        manager.setSubjectFactory(new JwtDefaultSubjectFactory());

        return manager;
    }


    /**
     * 由于需要自定义 filter chain,所以相关配置要配置在 ShiroFilterFactoryBean 中,而不是『页面跳转』式项目那样的 ShiroFilterChainDefinition 里。
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login-page"); // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403-page");

        /*
         * 定义Filter链
         */
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("anon", new AnonymousFilter());
        filters.put("jwt", new JwtFilter());    // 这里只能 new ,不能 @Autowired
        shiroFilterFactoryBean.setFilters(filters);

        /*
         * 拦截规则
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/login-page", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        // 认证靠 jwt-filter,鉴权靠注解。
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 但是 ShiroFilterChainDefinition 的配置仍然需要。
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        return new DefaultShiroFilterChainDefinition();
    }

//    @Bean
//    @DependsOn("lifecycleBeanPostProcessor")
//    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
//        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//         强制指定注解的底层实现使用 cglib 方案
//        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
//        return defaultAdvisorAutoProxyCreator;
//    }
}

相关文章

网友评论

      本文标题:Shiro 整合 JWT 实现无状态 Web 服务

      本文链接:https://www.haomeiwen.com/subject/wfqnkrtx.html