背景
前不久接触到一个新的项目,翻看了下项目框架和代码,发现很多地方用到了shiro,它不仅可以用来获取登录信息,还可以做权限校验。shiro的使用也极其方便,使用公共方法SecurityUtils.getSubject()便可获取Subject,通过它可以进行认证和授权相关的一系列操作,使用@RequiresPermissions注解就可以对请求进行权限校验。但对它的了解也仅仅是项目所展现的一知半解,对它还需要一个系统的了解,它的配置、使用以及底层。于是在网上查看了shiro教学的课程,并做了一个demo项目来实践。
但仅仅在认证成功之后的跳转上就出现了问题,并为此耗费了一周多的时间排查。。。特此记录下。
配置
pom.xml

shiro配置
package com.bruce.shiro.config;
import com.bruce.shiro.filter.ShiroFilter;
import com.bruce.shiro.security.CustomRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
return securityManager;
}
@Bean
public ShiroFilter formAuthenticationFilter() {
return new ShiroFilter();
}
/**
* 配置ShiroFilter过滤器
* @return ShiroFilter过滤器bean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//配置安全管理器
factoryBean.setSecurityManager(defaultWebSecurityManager());
Map<String, String> filterChainMap = new LinkedHashMap<>();
//配置系统公共资源
filterChainMap.put("/static/**", "anon");
//配置系统受限资源
filterChainMap.put("/user/**", "authc");
//配置认证请求
factoryBean.setLoginUrl("/user/login");
factoryBean.setSuccessUrl("/");
factoryBean.setFilterChainDefinitionMap(filterChainMap);
return factoryBean;
}
}
请求
@GetMapping("login")
public String login(UserEntity user, Model model) {
//设置当前登录对象
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
return "index";
} else if (StringUtils.isAnyBlank(user.getLoginName(), user.getPassword())) {
return "login";
}
try {
subject.login(new UsernamePasswordToken(user.getLoginName(), user.getPassword()));
} catch (UnknownAccountException ae) {
log.error("登录失败:{}", ae.getMessage());
model.addAttribute("msg", "用户名错误!");
return "login";
} catch (IncorrectCredentialsException pe) {
model.addAttribute("msg", "密码错误!");
return "login";
}
return "index";
}
最终效果
直接访问首页和登录的请求跳转没有问题,但登录认证成功后理论上应该跳转localhost:8080/,但总是跳转localhost:8080/index.html,由于index.html在templates目录,导致请求404,通过断点检查,是有走return "index"的,理论上thymeleaf解析后应该能找到/templates/index.html。
排查过程
第一次排查
以为是shiro认证问题,在网上也的确找到很多关于shiro认证成功跳转出错的文章,大部分的解决方案是继承FormAuthenticationFilter,重写issueSuccessRedirect,如图:
@Override
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
WebUtils.issueRedirect(request, response, getSuccessUrl());
}
修改后依然没有效果,断点检查发现根本没有进issueSuccessRedirect方法。根据该方法查找使用位置发现isLoginSubmission判断为false。
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
//此处判断为false
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
...
}
...
}
}
默认只有请求方式为POST时返回true
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
}
于是又重写了isLoginSubmission方法,加上了GET请求方式。之后又发现请求报认证失败,原来createToken中默认获取的是username和password的值进行认证,而我是将用户名存在loginName中,于是我又重写了createToken方法,让其获取loginName的属性值。
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//默认从request获取username属性值
String username = getUsername(request);
//默认从request获取password属性值
String password = getPassword(request);
return createToken(username, password, request, response);
}
最后认证成功,但依然跳转localhost:8080/index.html,请求404异常
第二次排查
我又想是不是thymeleaf配置问题,然后我又在网上找到很多关于使用thymeleaf无法找到templates目录下页面的文章。有说是配置了指定模板目录导致无法找到的,但我使用的是默认配置,页面也是放置在templates下。有说是thymeleaf依赖版本问题,与springboot版本不兼容导致,但我并没有配置版本,而是使用springboot的spring-boot-dependencies版本依赖管理,理论上不可能出现不兼容。本着死马当活马医的想法,我还是将我的springboot版本改为博主的版本,2.5.6改为2.4.1,看看会不会发生奇迹。然而奇迹并没有眷顾我。
第三次排查
没办法,我移除了shiro及thymeleaf依赖,决定先以较原始的方式让项目正常运行,然后再不断增加依赖,看看是哪个环节出了问题。问题来了,最终还是无法跳转。这让我改变了之前的看法,也许是前端请求的问题。于是我对比了跳转成功和不成功的请求,发现有那么一处不同,那就是登录请求是用的ajax方式调用的,我搜索之下才想起来,ajax请求是局部刷新,默认是不支持重定向的。我晕死,这个之前是学过的,我却没有想到,甚至特地用了ajax做登录请求,导致我在一个shiro认证上耗费一周多。。。
解决方案
将ajax方式的登录请求换成表单提交,shiro认证成功跳转,也不需要重写FormAuthenticationFilter,登录请求也可修改为如下:
@GetMapping("login")
public String login() {
return "login";
}
因为shiro认证登录在AuthenticationFilter的executeLogin就已进行,若认证成功则不会走登录请求,若认证失败才会走登录请求。但需要注意,若你的请求方式是GET或请求参数不是username、password、isRememberMe等默认参数,则必须重写FormAuthenticationFilter。可参考前文“第一次排查”所述。
总结
在正常工作中,由于多是使用现成的组件,以及架构大佬搭好的框架,所以我忽略了很多技术上的细枝末节,导致我会遇到一些看似奇奇怪怪的问题,其实问题奇奇怪怪,多半是自身技术不到位导致的。还是得多做练习,多自己搭项目,这样不仅会对已掌握的技术更加熟练,还会补全一些之前不知道的技术。
文章讲述了在使用Shiro进行认证和权限校验时,遇到登录成功后跳转异常的问题。经过排查,问题出在使用Ajax进行登录请求,导致无法重定向。解决方案是改用表单提交方式进行登录,避免了重定向问题。作者强调了对技术细节理解和实践的重要性。
1264

被折叠的 条评论
为什么被折叠?



