为什么写这篇文章
看过那么多框架、教程,大部分shiro的文章或教程是我见过思路最糟糕的。作者完不清楚想要表达什么起到什么作用,把大段大段的理论讲一通。你见过哪个java教程上来就给你讲一堆基础类库,讲虚拟机的。或者hibernate教程上来就给讲他有的设计有多精妙,管理的东西有多庞大的。
然后我曾经硬着头看了1周的所谓shiro教程,看完发现自己还是什么都不会,什么也做不出来。倍受打击。当年初学时看think in java都没这么失落过。
后来想想不对,就直接去找spring整合shiro的教程。折腾了一周总算做出来一个可以项目实用的东西了。但中间走过不少坑,其中有些可能是作者漏了,还有些是因为我也是整合shiro的要适应项目里的各个东西,适合自己项目的用法(这里吐槽一下,shiro会乱的原因就是配置的方式太多种了,而且好多文章都力求全面讲,对于一个项目真不需要全用到)。 不要跟我讲什么使用文本管理配置权限,什么写根据角色控制访问,哪个能用的项目会这么搞。浪费lz时间。 说什么从简单入手,你这个简单没鸟用,我后要改成从数据库读权限列表,读角色,根本就不可能在你这个简单的例子上逐渐改造,这还不是浪费时间还是什么,而且会用到你这框架的人,自己没几个项目拿来练手么。这不是浪费时间是什么。
另外各文章或教程,shiro的运行原理或者方式,只字未提,极力各种介绍概念。喂,我们不是搞学术的。后面看有些文章会把各个过程中加入作用说明和自己的理解的话,这个还挺不错的。但一直心里有一个疑问困扰着我,shiro是基于session(sessionId),还是基于tokken(每次访问都要传),因为我一直不知道前端要传什么参数,登录功能完成后,一直调不通登录之后的接口,初学时也很多东西不知道。讽刺的是这个答案要等自己调通了才知道。答案:shiro是基于session(sessionId),至少默认是这样的,tokken方式我没研究过(也不是我的菜,总感觉性能太差了,心生厌恶。流量不大,小项目还是session好用)。 真·心累。写这篇文章就是为了解救像我之前一样迷茫的同学。
解决什么问题
搭建一个项目可以用的shiro。 集成以下内容,使用相同内容的同学可以直接搬过去用了:
- spring,使用xml配置。喜欢注解配置的可以自己改造或找别的文章,个别文章我也是根据注解去配置xml的,看得懂就行。
- springMVC
- ACL(访问控制列表,用户角色权限都有),从数据库读取,在系统中是可配置的。
- 基于注解在contrllor方法之上的。这是我使用shiro的最初需求,因为restfull API中的通配符和path value,使用传统url拦截器(或过滤器),识别和判断很麻烦,检测太仔细又怕性能太差。
- ORM什么的随便,这个基本和shiro没关系,我这里用了hibernate
- 前后端分离,返回json数据。所以不需要跳转(未登录或没权限时)。
执行步骤
- 访问服务器,web.xml中的shiroFilter拦截,
- 拦截进入applicationContext-shiro.xml中配置的shiroFilter。在filterChainDefinitions中配置,/api/sys/login = anon 开放登陆接口,可以直接访问。
- 调用Login2Controller中的接口login方法,在方法中,账号和密码生成token,执行登录subject.login(token)
- 调用MyRealm#doGetAuthenticationInfo方法,验证登录信息。如果登录失败,抛出异常throw new UnknownAccountException("账号或密码错误")。如果登录成功,返回一个由用户信息、密码初始化的 SimpleAuthenticationInfo
- 回到login方法。处理登录失败和成功的情况:登录失败时这里直接抛出异常,会有@ControllerAdvice统一处理springmvc的异常,返回json数据。登录成功会把一些当前登录用户信息放到session中方便取用提高程序性能,如用户信息、部门、角色、权限等。要存在哪里看具体情况,普通sesssion、shiro的session,我直接存在principal里。到这里完成登录。
- 访问需要登录的接口,看applicationContext-shiro.xml中配置的shiroFilter,不是= anon的接口。会调用配置的shiroLoginFilter。
- 执行ShiroLoginFilter#isAccessAllowed方法,判断是否已登录。如果未登录执行下面的onAccessDenied方法,自定义返回结果。
- 如果访问的接口有配置(注解)权限,会调用MyRealm#doGetAuthorizationInfo方法,组织权限列表和controller方法上注解的@RequiresPermissions("user")匹配,匹配失败时会抛出UnauthorizedException异常,这个由@ControllerAdvice统一处理并返回json数据。如果匹配权限通过则正常返回接口执行结果。
- 退出登录,调用此方法SecurityUtils.getSubject().logout()。
示例
文件列表或涉及配置文件
- shiroFilter 在web.xml配置过滤器,shiro的入口
- applicationContext-shiro.xml,shiro在spring中的配置信息,由spring引入。
- MyRealm 自定义登录的判断doGetAuthenticationInfo(登录时执行一次),以及权限列表的组装doGetAuthorizationInfo(每次访问需要权限的接口都会执行)。在applicationContext-shiro.xml中配置
- ShiroLoginFilter.java,是否已登录判断和未登录的处理,这里是返回json。
- Login2Controller 登录的接口#login,还有登出的接口#logout,以及当前登录信息的获取。
关键代码
WEB-INF/web.xml,其他不相关内容省略
<web-app>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
applicationContext-shiro.xml,独立的文件引入到spring的配置中,可以在web.xml中引入也可以在总的applicationContext.xml import
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- id属性值要对应 web.xml中shiro的filter对应的bean -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"></property>
<property name="filters">
<util:map>
<entry key="authc" value-ref="shiroLoginFilter" />
</util:map>
</property>
<!-- 未登录跳转页面,请求地址将由formAuthenticationFilter进行表单认证 -->
<!-- 本项目通过ajax访问,由ShiroLoginFilter中处理返回json信息 -->
<!--<property name="loginUrl" value="/notLogin"></property>-->
<!-- 认证成功统一跳转到页面,建议不配置,shiro认证成功会默认跳转到上一个请求路径 -->
<!-- 本项目通过ajax访问,loginController#loging中直接返回json信息 -->
<!-- <property name="successUrl" value="/first.action"></property> -->
<!-- 通过unauthorizedUrl指定没有权限操作时跳转页面,这个位置会拦截不到,下面有给出解决方法 -->
<!-- 本项目通过ajax访问,由BaseController中@ExceptionHandler捕获异常处理 -->
<!--<property name="unauthorizedUrl" value="/refuse"></property>-->
<!-- 过滤器定义,从上到下执行,一般将/**放在最下面 -->
<property name="filterChainDefinitions">
<value>
<!-- 对静态资源设置匿名访问 -->
/assets/** = anon
<!--开放登陆接口-->
/api/sys/login = anon
/api/sys/logout = anon
/login.html = anon
<!-- /**=authc 所有的url都必须通过认证才可以访问 -->
/** = authc
<!-- /**=anon 所有的url都可以匿名访问,不能配置在最后一排,不然所有的请求都不会拦截 -->
</value>
</property>
</bean>
<!--使用ajax访问,自定义未登录返回信息-->
<bean id="shiroLoginFilter" class="com.hammer.acl.shiro.ShiroLoginFilter"></bean>
<!-- 解决shiro配置的没有权限访问时,unauthorizedUrl不跳转到指定路径的问题 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
</props>
</property>
</bean>
<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"></property>
</bean>
<!-- 配置自定义Realm -->
<bean id="myRealm" class="com.hammer.acl.shiro.MyRealm">
<!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
</bean>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 加密算法 -->
<property name="hashAlgorithmName" value="md5"></property>
<!-- 迭代次数 -->
<property name="hashIterations" value="1"></property>
</bean>
</beans>
MyRealm.java
/**
* 自定义的Realm
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private LoginService loginService;
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}
/**
* 认证的方法,登录时执行
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//System.out.println("————身份认证方法————");
// token是用户输入的用户名和密码
// 第一步从token中取出用户名
final String loginId = (String) token.getPrincipal();
String password = null;
final Object credentials = token.getCredentials();
if (credentials instanceof char[]) {
password = new String((char[]) credentials);
}
// 第二步:根据用户输入从数据库查询用户信息
User user = loginService.getUse4Login(loginId, password);
if (user == null) {
throw new UnknownAccountException("账号或密码错误");
}
// 从数据库查询到密码
//配合shiro配置的mc5加密(应该可以配置为不加密)
if (password != null) {
password = DigestUtils.md5Hex(password);
}
//加密的盐
//String salt = user.getSalt();
final HashMap<String, Object> principal = new HashMap<>();
principal.put("user", user);
return new SimpleAuthenticationInfo(principal, password, this.getName());
}
/**
* 授权的方法,每次访问需要权限的接口都会执行
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//System.out.println("————权限认证————");
//从principals获取主身份信息
//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中的身份类型)
//以下方法等效SecurityUtils.getSubject().getPrincipal() principals.getPrimaryPrincipal()
//Map principal = (Map) SecurityUtils.getSubject().getPrincipal();
Map principal = (Map) principals.getPrimaryPrincipal();
User user = (User) principal.get("user");
List<String> permissions = (List<String>) principal.get("permissions");
//查到权限数据,返回授权信息(要包括上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);//这里添加用户有的权限列表
simpleAuthorizationInfo.addRole(user.getRoleId());//这里添加用户所拥有的角色
return simpleAuthorizationInfo;
}
}
ShiroLoginFilter.java
public class ShiroLoginFilter extends FormAuthenticationFilter {
private static final Logger log = LoggerFactory.getLogger(ShiroLoginFilter.class);
/**
* 如果isAccessAllowed返回false 则执行onAccessDenied
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
boolean isAllowed = false;
//前端(某些框架)测试接口(OPTIONS)直接放行
if (request instanceof HttpServletRequest) {
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
isAllowed = true;
}
}
isAllowed = super.isAccessAllowed(request, response, mappedValue);
if (isAllowed) {
//登录状态,作一些日志记录
}
return isAllowed;
}
/**
* 未登录时的处理
*
* @param request
* @param response
* @return true-继续往下执行,false-该filter过滤器已经处理,不继续执行其他过滤器
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
log.info("用户未登录");
final HttpServletRequest request2 = (HttpServletRequest) request;
final HttpServletResponse response2 = (HttpServletResponse) response;
//ajax访问接口返回数据结构
if (WebUtil.isAjax(request2)) {// ajax接口
//这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
response2.setHeader("Access-Control-Allow-Origin", request2.getHeader("Origin"));
response2.setHeader("Access-Control-Allow-Credentials", "true");
response2.setCharacterEncoding("UTF-8");
response2.setContentType("application/json");
Map responseData = new HashMap();
responseData.put("state", "unauthorized");
responseData.put("code", 401);
responseData.put("msg", "用户未登录");
String result = Json.toJson(responseData);
PrintWriter out;
try {
out = response2.getWriter();
out.print(result.toString());
out.flush();
} catch (IOException e) {
log.error("返回数据失败!", e);
}
} else {
//其他情况
//shiro处理
super.onAccessDenied(request, response);
//其他处理方式
// 页面,直接跳转登录页面
//redirect("login.html", request2, response2);
//web.xml处理
//response2.setStatus(401);// 客户试图未经授权访问受密码保护的页面。
}
return false;
}
}
Login2Controller.java
/**
* shiro登录
*/
@Slf4j
@RestController
@RequestMapping("/api/sys")
public class Login2Controller{
@Autowired
private LoginService service;
@Autowired
private LoginService loginService;
/**
* 登陆
*
* @param loginId 登录账号
* @param password 密码
*/
@RequestMapping(value = "/login")
public RespObject login(String loginId, String password, HttpServletRequest req) {
final String host = req.getRemoteHost();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(loginId, password, host);
try {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 执行认证登陆
subject.login(token);
//set session attribute
final Map principal = (Map) subject.getPrincipal();
User user = (User) principal.get("user");
// loginService.buildSessionAttr方法生成了包含 List<String> permissions,key为"permissions";
final Map sessionAttrs = loginService.buildSessionAttr(user);
principal.putAll(sessionAttrs);
} catch (UnknownAccountException e) {
final String message = e.getMessage();
log.info(String.format("%s[%s/%s]", message, loginId, password));
throw new FailException(message);
} catch (AuthenticationException e) {
throw new FailException(e);
}
return RespObject.success(null, "登录成功");
}
/**
* 退出
*
* @return
*/
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public RespObject logout() {
Subject subject = SecurityUtils.getSubject();
//注销
subject.logout();
return RespObject.success(null, "成功注销!");
}
/**
* 当前session属性
*
* @param attr
* @return
*/
@RequestMapping(value = "/current")
public RespObject getCurrentAttr(String attr) {
Map sessionAttrs = service.readSessionAttr();
if (Strings.isEmpty(attr)) {
return RespObject.success(sessionAttrs);
} else {
return RespObject.success(sessionAttrs.get(attr));
}
}
其他代码
MyControllerAdvice.java,统一处理spring MVC异常,代码里有些调用别地的常用处理方法,根据实际情况修改。
/**
* controller 增强器,应用到所有@RequestMapping注解方法
*/
@ControllerAdvice
public class MyControllerAdvice {
private static final Logger log = LoggerFactory.getLogger(MyControllerAdvice.class);
@ExceptionHandler
@ResponseBody
public Object errorHandler(HttpServletRequest request, Exception e, HttpServletResponse response) {
//记录日志
if (e instanceof UnauthorizedException) {
//没有权限
String uri = request.getServletPath();
final String queryString = request.getQueryString();
if (null != queryString && queryString.trim().length() > 0) {
uri = uri + "?" + queryString;
}
log.info(String.format("%s, [uri = %s]", e.getMessage(), uri));
} else {
if (e instanceof BaseException) {
log.error(e.getMessage());
} else {
log.error("异常错误", e);
}
}
Throwable e2 = WebUtil.deepestException(e);
try {
// 是否ajax调用
boolean isAjax = true;
if (WebUtil.isAjax(request)) {
RespObject respObject;
if (e instanceof UnauthorizedException) {
respObject = RespObject.forbidden();
} else if (e instanceof FailException) {
respObject = RespObject.fail(RespObject.getExceptionMessage(e2));
} else if (e instanceof ErrorException) {
respObject = RespObject.error(e2);
} else {
respObject = RespObject.exception(e2);
}
respObject.setExtra(e2.getMessage());
return respObject;
} else {
// 添加自己的异常处理逻辑,如日志记录
request.setAttribute("exceptionMessage", e.getMessage());
return "common/error";
}
} catch (Exception e3) {
log.error("返回数据失败!", e3);
}
return "common/error";
}
}
UserController.java,测试接口调用
@RestController
@RequestMapping("/api/base/user")
public class UserController extends BaseController<User, String> {
@RequiresPermissions(value = {"user"}, logical = Logical.OR)//执行此方法需要权限
@RequestMapping(value = "/search")
public RespObject search(PageParam pageParam, User bean) {
return super.search(pageParam, bean);
}
}
其他注意
- 有一个坑是applicationContext-shiro.xml中,shiroFilter <property name="filters"> 配置的<entry key="authc" value-ref="shiroLoginFilter" />,这里的key="authc"不能随便乱写,有些文章是随便写的,会导致不会调shiroLoginFilter.java中的方法。
引入jar包或版本信息
spring相关的包怎么引的随便找,使用shiro这里aop,肯定要
<java.version>1.8</java.version>
<spring.version>5.1.7.RELEASE</spring.version>
<hibernate.version>5.4.2.Final</hibernate.version>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>