Shiro 学习应用(续)

本文详细阐述了Shiro框架中表单过滤器的作用及其与控制器的关系,重点分析了如何在实现验证码组件时利用表单过滤器,并提供了Shiro认证流程的总结。此外,文章还分享了扩展表单过滤器以实现验证码功能的代码示例,以及表单POST字段映射的配置方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面的文章中为大家介绍了 Shrio 的基础概念,可能比较笼统,没有深入到开发过程的一些问题。现在集中在本帖中归纳一下有关问题。

FormAuthenticationFilter 表单过滤器

表单过滤器的问题,是本人在实现验证码组件时候遇到的,亦曾经一度让我“抓狂”。虽然有便捷的方法实现验证码,例如在控制器中就可以判断验证码逻辑了,但是那有违 Shiro 结构体系的思想;本人也想籍此了解 Shiro 扩展实现方法,——如果都走捷径,那么学习的目的就达不到了。于是,本人就把遇到问题逐一罗列出来。

首先啰嗦下,为什么不写在控制器?

Shiro 提供丰富的过滤器,FormAuthenticationFilter 就是一种过滤器。既然使用了安全框架,那么我们就应该按照框架的方法去编码。我们知道,经典 Web MVC 中,过滤器是优于 Servlet 执行的,也是一般用作安全检测、权限校验之用的。所以 Shiro 组件基于过滤器的思想就“名正言顺”了(所以说 Filter 能做的,Shiro 也能做,Filter 做不了的或者不适合做的, Shiro 过滤器也不会僭越)。实际从作用功效上,过滤器与 Servlet 彼此替代可能性蛮大的,也就是说,Servlet 的逻辑用在 Filter 之上亦可以,效果看起来不会差太多——反之亦然。不过,我们还是要回顾下它们之间的区别:首先是前面说的,Filter 优于 Servlet 执行,Filter 拦截了,Servlet 也不会执行;其次,编码风格上 Filter 基于责任链式模式,Servlet 却不是;最后就是 Filter 不像 Servlet,它不能产生一个请求或者响应,Filter 通常只是返回 true/false。当然,它们是如此地相像,以至于把 Filter 认为是 Servlet 变种的一种亦未尝不可。

所以,我们会看到 Shiro 的一些案例中,既有 Filter,又有 Controller。例如下面我们绑定会员登录的 url 到 FormAuthenticationFilter,然后又声明有控制器。

CaptchaFormAuthenticationFilter 是扩展 FormAuthenticationFilter 的验证码过滤器。/service/user/access/login = authcaptcha 这句就是绑定了验证码过滤器。这样如此,应该是不用编码就可以调用 CaptchaFormAuthenticationFilter 了。

这个完整的请求还有 Controller 部分:

@RequestMapping(value = "/login", method = RequestMethod.POST)
public void loginAction(User user, HttpServletRequest request, HttpServletResponse response) {
	String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
	Throwable exObj = (Throwable) request.getAttribute("exObj");

	String error = null;
	LOGGER.info("客户端 " + user.getName() + "登录..." + exceptionClassName);
	
	if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
		error = "未知用户错误";
	} else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
		error = "用户名/密码错误";
	} else if (IncorrectCaptchaException.class.getName().equals(exceptionClassName)) {
		error = "验证码错误";
	} else if (exObj != null && exObj.getClass().getName().equals(IllegalArgumentException.class.getName())) {
		error = exObj.getMessage(); // 缺少某个字段
	} else if (exceptionClassName != null) {
		error = "其他错误:" + exceptionClassName;
	}
	
	// 输出 JSON
	ResponseHelper rsp = new ResponseHelper(response);
	boolean isOk = error == null;
	String msg = error == null ? "登录成功!" : error + "具体原因:" + (exObj != null ? exObj.getMessage() : "N/A");
	rsp.outputAction(isOk, msg);
}

可见,这个控制器好像“打杂”的,作用比较简单,像是一些善后的工作。

但是,为什么过滤器执行不起来?

遗憾的是,声明过滤器之后却执行不起来,不能正确跳转。访问这个 url,即使输入合法也跳到“登录”页面(也就是未登录的页面)。然后反复搜索相关资料和例子,得知表单是 AccessControlFilter 的子类,如果访问了一定回 isAccessAllowed() 方法,试了下果然可以,表示是否允许访问,然后我下意识地调用验证码的 executeLogin() 方法,也就是封装的 getSubject().login(token); 的方法。难道是这样调用控制器的吗?我覺得我这种方法有点“简单粗暴”,框架不至于推荐这么做——后来的测试也证明此法不可行,报了一个两次 redirect 重定向的异常。——这肯定是我的方法不对!

百搜不得其解下,我十分灰心,甚至想放弃,后来我想到把 Shiro 源码和文档附加到 jar 中,看看里面的源码和注释。果然人家就说明需要 POST 请求,而且要指定登陆 URL(这个是 Action 接受请求的地址,如果不设置默认是 /login.jsp,这就是导致很多人不能成功调用过滤器的原因!)才能调用过滤器,有条件限制的,而且根本不需要执行 isAccessAllowed() 方法。

后来又发现个小插曲,——偶尔登录成功后,在尝试登录是不会执行过滤器。因此 Shiro 定义是“一次执行的”,具体原理我就没有深究了只是知道有这么一个机制。测试对应的方法也很简单,把浏览器缓存清掉,令其 session 去掉。

另外表单 POST 字段的映射,FormAuthenticationFilter 也考虑到了,参见配置中的 <property name="usernameParam" value="name" />:

	<bean id="captchaFormAuthenticationFilter"
		class="com.ajaxjs.framework.user.captcha.CaptchaFormAuthenticationFilter">
		<property name="usernameParam" value="name" />
		<property name="loginUrl" value="/service/user/access/login" />
	</bean>

这里的是 Spring 的配置方法。

小结一下 Shiro 认证

Shiro 就是这样,提供现成的组件方便我们调用,我们也可以从中了解其机制,——反正都是开源的。过滤器是认证的第一板斧,是创建 token 的那一步。有了 token 才能执行 AuthenticatingRealm.doGetAuthenticationInfo(AuthenticationToken authcToken),最后到控制器。网上有些例子把创建 token 那一步写在控制器中,尽管做法有点简单粗暴,但道理还是一样的。

附录,图片验证码过滤器:

/**
 * Copyright 2015 Sp42 frank@ajaxjs.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ajaxjs.user.auth.captcha;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import com.ajaxjs.user.user.controller.WebController;
import com.ajaxjs.util.StringUtil;
import com.ajaxjs.util.logger.LogHelper;

/**
 * 扩展 FormAuthenticationFilter 接下来我们扩展 FormAuthenticationFilter 类, 首先覆盖
 * createToken 方法,以便获取 CaptchaUsernamePasswordToken 实例; 然后增加验证码校验方法
 * doCaptchaValidate; 最后覆盖 Shiro 的认证方法 executeLogin,在原表单认证逻辑处理之前进行验证码校验。
 * 
 * @author Sp42 frank@ajaxjs.com
 *
 */
public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter {
	private static final LogHelper LOGGER = LogHelper.getLog(CaptchaFormAuthenticationFilter.class);

	/**
	 * SESSION 的键值
	 */
	public static final String SESSION_KEY = "rand";

	private boolean captchaEnabled = true;// 是否开启验证码支持,默认支持

	private static final String submitedFieldName = "captchaCode";

	//	@Override
	//	public boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) {
	//		System.out.println(getLoginUrl());
	//		System.out.println("::::::::"+isLoginSubmission(req, response));
	//		System.out.println("::::::::"+isLoginRequest(req, response));
	//		req.setAttribute("captchaEbabled", captchaEnabled);
	//		return true;
	//	}

	/**
	 * 认证
	 * 
	 * @throws Exception
	 */
	@Override
	protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
		LOGGER.info("开始验证码校验……");
		CaptchaUsernamePasswordToken token = null;

		try {
			token = createToken(request, response);
		} catch (IllegalArgumentException e) {
			AuthenticationException ae = new AuthenticationException(e.getMessage());// 封装一下
			request.setAttribute(WebController.request_exception_key, e);
			return onLoginFailure(token, ae, request, response);
		}

		//		System.out.println("login::" + isCaptchaEnabled());

		try {
			// 验证码校验
			// 判断验证码是否禁用,如果是,跳过验证码,直接处理表单
			if (isCaptchaEnabled()) {
				System.out.println(11111);
				HttpServletRequest req = (HttpServletRequest) request;
				String captcha = (String) req.getSession().getAttribute(SESSION_KEY);

				if (captcha == null) {
					throw new IncorrectCaptchaException("丢失验证码,请稍后刷新验证码后再试!");
				}

				if (captcha != null && !captcha.equalsIgnoreCase(token.getCaptcha())) { // 用户输入的验证码

					throw new IncorrectCaptchaException("验证码错误!");
				}

				System.out.println(22222);
				LOGGER.info("验证码" + captcha + "正确");
			}

			System.out.println(333);
			Subject subject = getSubject(request, response);
			subject.login(token);

			System.out.println(444);
			//			return onLoginSuccess(token, subject, request, response);
			return true; // 返回 true 让过滤器继续(而不是调到默认的 welcome),返回的 controller 那里去,因为我要在那裏返回 json !
		} catch (AuthenticationException e) {
			e.printStackTrace();
			request.setAttribute(WebController.request_exception_key, e);
			return onLoginFailure(token, e, request, response);
		}
	}

	/**
	 * 判断用户输入的验证码是否通过
	 * 
	 * @param request
	 * @param CaptchaCode
	 * @return true 表示通过
	 * @throws Throwable
	 */
	public static boolean isPass(HttpServletRequest request, String CaptchaCode) throws Throwable {
		boolean isCaptchaPass = false;

		String rand = (String) request.getSession().getAttribute(SESSION_KEY);

		System.out.println("rand:" + rand);
		System.out.println("CaptchaCode:" + CaptchaCode);

		if (rand == null)
			throw new UnsupportedOperationException("请刷新验证码。");
		else if (CaptchaCode == null || CaptchaCode.equals("")) {
			throw new IllegalArgumentException("没提供验证码参数");
		} else {
			isCaptchaPass = rand.equals(CaptchaCode);
			if (!isCaptchaPass)
				throw new IllegalAccessError("验证码不正确");
		}

		if (isCaptchaPass) {
			request.getSession().removeAttribute(SESSION_KEY);// 通过之后记得要 清除验证码
		}

		return isCaptchaPass;
	}

	/**
	 * 创建 Token
	 */
	@Override
	protected CaptchaUsernamePasswordToken createToken(ServletRequest request, ServletResponse response) {
		String username = getUsername(request);
		String password = getPassword(request);

		// 用户输入的验证码
		String captcha = request.getParameter(submitedFieldName);

		if (StringUtil.isEmptyString(captcha)) {
			throw new IllegalArgumentException("缺少参数!" + submitedFieldName);
		} else {
			captcha = captcha.trim();
		}

		boolean rememberMe = isRememberMe(request);
		String host = getHost(request);

		if (StringUtil.isEmptyString(username)) {
			throw new IllegalArgumentException("缺少 username 参数!");
		}

		if (StringUtil.isEmptyString(password)) {
			throw new IllegalArgumentException("缺少 password 参数!");
		}

		System.out.println(username);
		System.out.println(password);

		return new CaptchaUsernamePasswordToken(username, password.toCharArray(), rememberMe, host, captcha);
	}

	public boolean isCaptchaEnabled() {
		return captchaEnabled;
	}

	public void setCaptchaEnabled(boolean captchaEnabled) {
		this.captchaEnabled = captchaEnabled;
	}
}
/**
 * Copyright 2015 Sp42 frank@ajaxjs.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ajaxjs.user.auth.captcha;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * Shiro 表单认证,页面提交的用户名密码等信息,用 UsernamePasswordToken 类来接收,很容易想到,
 * 要接收页面验证码的输入,我们需要扩展此类
 * 
 * @author Sp42 frank@ajaxjs.com
 *
 */
public class CaptchaUsernamePasswordToken extends UsernamePasswordToken {
	private static final long serialVersionUID = 1L;
	private String captcha;

	/**
	 * 
	 * @param username
	 * @param password
	 * @param rememberMe
	 * @param host
	 * @param captcha
	 */
	public CaptchaUsernamePasswordToken(String username, char[] password, boolean rememberMe, String host, String captcha) {
		super(username, password, rememberMe, host);
		this.setCaptcha(captcha);
	}

	public String getCaptcha() {
		return captcha;
	}

	public void setCaptcha(String captcha) {
		this.captcha = captcha;
	}
}

 

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值