开源项目学习-jeesite1.2.7-登录功能分析
写在前面
通过查看部分源码,发现登录功能是与Shiro深度结合的,但是这个Shiro我又没学,就很烦,所以先待我学习一波。
Shiro已学完,今天开始分析登录功能。
功能介绍
账号system,密码jeesite.com
参考资料
shiro之深度解析FormAuthenticationFilter
jeesite用户登录功能解析,这篇文章已经写的挺详细了,下面讲一下其它细节。
分析
在LoginController中,login和loginFail两个函数就是处理form的函数,或者说正常情况下是由这两个函数来处理。但是仔细看这两个函数,并没有进行逻辑处理,只是简单的检查和跳转。这是因为shiro的登陆功能在controller之前加入了一个filter,用户校验信息被保存在了SystemAuthorizingRealm类中的Principal内部类中,在controller中获取之后进行进一步操作。
shiro认证部分
认证过程与学习资料中展示的例子有所不同,因为例子中直接通过subject进行了登录操作,而在jeestie项目中,用户名,密码等信息首先由formAuthenticatingFilter这个类处理的,并重写了父类中的一些方法。其父类执行了jubject.login(token),通过SystemAuthorizingRealm进行认证操作,返回的信息由formAuthenticatingFilter处理.
在formAuthenticationFilter中,重写的createToken函数会获取表单中的用户名称和密码,生成一个自定义的token。
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
if (password==null){
password = "";
}
boolean rememberMe = isRememberMe(request);
String host = StringUtils.getRemoteAddr((HttpServletRequest)request);
String captcha = getCaptcha(request);
boolean mobile = isMobileLogin(request);
return new UsernamePasswordToken(username, password.toCharArray(), rememberMe, host, captcha, mobile);
}
然后调用父类方法executeLogin方法,首先调用了createToken方法获取token,然后调用subject.login(token)开始认证。
public abstract class AuthenticatingFilter extends AuthenticationFilter {
public static final String PERMISSIVE = "permissive";
public AuthenticatingFilter() {
}
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
}
subject.login(token)会调用SystemAuthorizingRealm中的doGetAuthenticationInfo进行认证。
/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
if (logger.isDebugEnabled()){
logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, token.getUsername());
}
// 校验登录验证码
if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
Session session = UserUtils.getSession();
String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
throw new AuthenticationException("msg:验证码错误, 请重试.");
}
}
// 校验用户名密码
User user = getSystemService().getUserByLoginName(token.getUsername());
if (user != null) {
if (Global.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("msg:该已帐号禁止登录.");
}
byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()),
user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
} else {
return null;
}
}
先进行了验证码校验,如果认证失败次数大于等于3,则需要输入验证码,首先判断是否认证失败大于等于3,如果是,则获取当前用户的Session,然后获取其中保存的验证码,最后进行验证码校验。验证码如何生成?
之后就是校验用户名密码,在SystemAutnorizingRealm中有systemService的实例,该实例中的userDao能取出数据库中的name和password。
回到formAuthenticationFilter,如果认证失败,则会抛出AuthenticationException异常,通过catch捕获该异常后,会调用onLoginFailure方法,这个方法在FormAuthenticationFilter中进行了重写。
/**
* 登录失败调用事件
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request, ServletResponse response) {
String className = e.getClass().getName(), message = "";
if (IncorrectCredentialsException.class.getName().equals(className)
|| UnknownAccountException.class.getName().equals(className)){
message = "用户或密码错误, 请重试.";
}
else if (e.getMessage() != null && StringUtils.startsWith(e.getMessage(), "msg:")){
message = StringUtils.replace(e.getMessage(), "msg:", "");
}
else{
message = "系统出现点问题,请稍后再试!";
e.printStackTrace(); // 输出到控制台
}
request.setAttribute(getFailureKeyAttribute(), className);
request.setAttribute(getMessageParam(), message);
return true;
}
这个方法主要就是把抛出的异常以及对应的信息写回了request中。
所以整个登录逻辑为:如果任何地方未登录,则访问登录页面,提交时先通过formAuthenticationFilter过滤器,验证账号密码,然后交给LoginController处理。
认证成功流程
/**
* 管理登录
*/
@RequestMapping(value = "${adminPath}/login", method = RequestMethod.GET)
public String login(HttpServletRequest request, HttpServletResponse response, Model model) {
Principal principal = UserUtils.getPrincipal();
// // 默认页签模式
// String tabmode = CookieUtils.getCookie(request, "tabmode");
// if (tabmode == null){
// CookieUtils.setCookie(response, "tabmode", "1");
// }
if (logger.isDebugEnabled()){
logger.debug("login, active session size: {}", sessionDAO.getActiveSessions(false).size());
}
// 如果已登录,再次访问主页,则退出原账号。
if (Global.TRUE.equals(Global.getConfig("notAllowRefreshIndex"))){
CookieUtils.setCookie(response, "LOGINED", "false");
}
// 如果已经登录,则跳转到管理首页
if(principal != null && !principal.isMobileLogin()){
return "redirect:" + adminPath;
}
// String view;
// view = "/WEB-INF/views/modules/sys/sysLogin.jsp";
// view = "classpath:";
// view += "jar:file:/D:/GitHub/jeesite/src/main/webapp/WEB-INF/lib/jeesite.jar!";
// view += "/"+getClass().getName().replaceAll("\\.", "/").replace(getClass().getSimpleName(), "")+"view/sysLogin";
// view += ".jsp";
return "modules/sys/sysLogin";
}
认证成功逻辑,首先通过调用sessionDAO.getActiveSessions(false).size()获取了在线的用户的数量并打印。
if (logger.isDebugEnabled()){
logger.debug("login, active session size: {}", sessionDAO.getActiveSessions(false).size());
}
下面这个不知道有啥用,待补。
// 如果已登录,再次访问主页,则退出原账号。
if (Global.TRUE.equals(Global.getConfig("notAllowRefreshIndex"))){
CookieUtils.setCookie(response, "LOGINED", "false");
}
认证失败流程
/**
* 登录失败,真正登录的POST请求由Filter完成
*/
@RequestMapping(value = "${adminPath}/login", method = RequestMethod.POST)
public String loginFail(HttpServletRequest request, HttpServletResponse response, Model model) {
Principal principal = UserUtils.getPrincipal();
// 如果已经登录,则跳转到管理首页
if(principal != null){
return "redirect:" + adminPath;
}
String username = WebUtils.getCleanParam(request, FormAuthenticationFilter.DEFAULT_USERNAME_PARAM);
boolean rememberMe = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM);
boolean mobile = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_MOBILE_PARAM);
String exception = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
String message = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM);
if (StringUtils.isBlank(message) || StringUtils.equals(message, "null")){
message = "用户或密码错误, 请重试.";
}
model.addAttribute(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM, username);
model.addAttribute(FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM, rememberMe);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MOBILE_PARAM, mobile);
model.addAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME, exception);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM, message);
if (logger.isDebugEnabled()){
logger.debug("login fail, active session size: {}, message: {}, exception: {}",
sessionDAO.getActiveSessions(false).size(), message, exception);
}
// 非授权异常,登录失败,验证码加1。
if (!UnauthorizedException.class.getName().equals(exception)){
model.addAttribute("isValidateCodeLogin", isValidateCodeLogin(username, true, false));
}
// 验证失败清空验证码
request.getSession().setAttribute(ValidateCodeServlet.VALIDATE_CODE, IdGen.uuid());
// 如果是手机登录,则返回JSON字符串
if (mobile){
return renderString(response, model);
}
return "modules/sys/sysLogin";
}
认证失败逻辑,首先获取了principal,然后判断是否为空,既然认证失败了,为啥会不为空?待补
Principal principal = UserUtils.getPrincipal();
// 如果已经登录,则跳转到管理首页
if(principal != null){
return "redirect:" + adminPath;
}
接下来是从request中获取信息,并进行了校验。当认证失败时,shiro会调用AuthenticatingFilter中的onLoginFailure方法,这也是为什么message中会有信息。如果message位空,则会写入用户或密码错误, 请重试
String username = WebUtils.getCleanParam(request, FormAuthenticationFilter.DEFAULT_USERNAME_PARAM);
boolean rememberMe = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM);
boolean mobile = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_MOBILE_PARAM);
String exception = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
String message = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM);
if (StringUtils.isBlank(message) || StringUtils.equals(message, "null")){
message = "用户或密码错误, 请重试.";
}
然后又将这些信息写回了model中
model.addAttribute(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM, username);
model.addAttribute(FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM, rememberMe);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MOBILE_PARAM, mobile);
model.addAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME, exception);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM, message);
接下来是对验证码的一些操作。首先判断认证异常信息是否是调用了,如果是,则调用isValidateCodeLogin(username, true, false)。之后清空seesion中的验证码。
// 非授权异常,登录失败,验证码加1。
if (!UnauthorizedException.class.getName().equals(exception)){
model.addAttribute("isValidateCodeLogin", isValidateCodeLogin(username, true, false));
}
// 验证失败清空验证码
request.getSession().setAttribute(ValidateCodeServlet.VALIDATE_CODE, IdGen.uuid());
先来看看isValidateCodeLogin(username, true, false),这个方式在LoginController中进行了定义,这段代码主要的作用就是将用户认证失败的次数写入CacheUtils中,如果失败次数大于等于3,则返回true。返会的参数被置入了model。
/**
* 是否是验证码登录
* @param useruame 用户名
* @param isFail 计数加1
* @param clean 计数清零
* @return
*/
@SuppressWarnings("unchecked")
public static boolean isValidateCodeLogin(String useruame, boolean isFail, boolean clean){
Map<String, Integer> loginFailMap = (Map<String, Integer>)CacheUtils.get("loginFailMap");
if (loginFailMap==null){
loginFailMap = Maps.newHashMap();
CacheUtils.put("loginFailMap", loginFailMap);
}
Integer loginFailNum = loginFailMap.get(useruame);
if (loginFailNum==null){
loginFailNum = 0;
}
if (isFail){
loginFailNum++;
loginFailMap.put(useruame, loginFailNum);
}
if (clean){
loginFailMap.remove(useruame);
}
return loginFailNum >= 3;
}
然后是将生成的IdGen.uuid()放入Session中,对应的变量名为ValidateCodeServlet.VALIDATE_CODE,这么做的作用就是给session中的验证码赋一个随机值,变相清空验证码。
public class ValidateCodeServlet extends HttpServlet {
public static final String VALIDATE_CODE = "validateCode";
//...
}
/**
* 封装各种生成唯一性ID算法的工具类.
* @author ThinkGem
* @version 2013-01-15
*/
@Service
@Lazy(false)
public class IdGen implements IdGenerator, SessionIdGenerator {
private static SecureRandom random = new SecureRandom();
/**
* 封装JDK自带的UUID, 通过Random数字生成, 中间无-分割.
*/
public static String uuid() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
//...
}
可以在登录页sysLogin.jsp中看到一下代码:
$(document).ready(function() {
$("#loginForm").validate({
rules: {
validateCode: {remote: "${pageContext.request.contextPath}/servlet/validateCodeServlet"}
},
messages: {
username: {required: "请填写用户名."},password: {required: "请填写密码."},
validateCode: {remote: "验证码不正确.", required: "请填写验证码."}
},
errorLabelContainer: "#messageBox",
errorPlacement: function(error, element) {
error.appendTo($("#loginError").parent());
}
});
});
感觉是请求了${pageContext.request.contextPath}/servlet/validateCodeServlet这个地址。通过全局搜索validateCodeServlet,在web.xml中发现了。
<!-- Validate code -->
<servlet>
<servlet-name>ValidateCodeServlet</servlet-name>
<servlet-class>com.thinkgem.jeesite.common.servlet.ValidateCodeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ValidateCodeServlet</servlet-name>
<url-pattern>/servlet/validateCodeServlet</url-pattern>
</servlet-mapping>
从上图可以得出验证码就是通过servlet获取的。
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
createImage(request,response);
}
private void createImage(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
/*
* 得到参数高,宽,都为数字时,则使用设置高宽,否则使用默认值
*/
String width = request.getParameter("width");
String height = request.getParameter("height");
if (StringUtils.isNumeric(width) && StringUtils.isNumeric(height)) {
w = NumberUtils.toInt(width);
h = NumberUtils.toInt(height);
}
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
/*
* 生成背景
*/
createBackground(g);
/*
* 生成字符
*/
String s = createCharacter(g);
request.getSession().setAttribute(VALIDATE_CODE, s);
g.dispose();
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.close();
}
生成的验证存放到了Session里,以供认证器检查。
/*
* 生成字符
*/
String s = createCharacter(g);
request.getSession().setAttribute(VALIDATE_CODE, s);
收获
代码千万条,安全第一条,从jeesite的登录流程可以看出,之前项目中写的登录就跟*一样,完全没有考虑安全问题。
配置文件
例子中的配置文件是写死在ShrioConfig中的,而jeesite则是将这些信息写入了配置文件中
<!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
/act/editor/** = user
/ReportServer/** = user
</value>
</constructor-arg>
</bean>
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${adminPath}/login" />
<property name="successUrl" value="${adminPath}?login" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
其中指定了${adminPath}/login的验证权限名为authc的过滤器。 authc对应的filter为formAuthenticationFilter。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)