Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。相比较Spring Security,shiro有小巧、简单、易上手等的优点。所以很多框架都在使用shiro。而springboot作为一个开源框架,必然提供了和shiro整合的功能!接下来就用springboot整合shiro完成对于用户登录的判定和权限的验证.
1.基础数据
公司项目采用的spring-boot框架。在做用户权限功能的时候准备采用shiro权限框架。前面也考虑过spring家族的spring security安全框架。但是经过网上查询对比最终选择了shiro。因为shiro含有基本的安全控制功能,并且配置更为简单,使用也更加简洁。
首先引入shiro依赖jar包
<dependency>
<groupId></groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro缓存插件-->
<dependency>
<groupId></groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
本次shiro插件缓存功能实现采用的是ehcahe。前面也尝试过使用Redis。但是在配置数据源那块报错,无法解决数据源的问题。所以直接改用了ehcahe。后面如果解决了数据源问题。再发整合Redis的教程。
首先我在系统建立用户权限关系
这里我们需要三张表:
SysUser: 用来存储用户的密码,用户名等等信息。
SysRole: 角色表,存放所有的角色信息
SysAuth:权限表,定义了一些操作访问权限信息。
还有两张关联表(这里我们用JPA自动生成。):
SysUserRole: SysUser和SysRole的关联表。
SysRoleAuth:SysRole和SysAuth的关联表。
这里贴三张表的字段设计
public class SysUser {
private Integer userId;
private String userAccount;//用户账号
private String userPassword;//用户密码
}
public class SysRole {
private Integer sysRoleId;
private Byte sysRoleAva; //角色是否生效
private String sysRoleDes;//角色描述
private String sysRoleName;//角色名称
}
public class SysAuth {
private Integer sysAuthId;
private String sysAuthCode; //权限编号
private String sysAuthName; //权限名称
private String sysAuthUrl; //权限请求的url 例如: user/login
private String sysAuthPermission; //权限的的名称例如 user:login
private Byte sysAuthAva; //权限是否有效
private Byte sysAuthType; //权限类型。菜单还是按钮
private String sysAuthDes; //权限描述
}
1.1 配置Realms
Realm是一个Dao,通过它来验证用户身份和权限。这里Shiro不做权限的管理工作,需要我们自己管理用户权限,只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。Shiro就会自动的进行权限判断。在项目包下建一个ShiroRealm类,继承AuthorizingRealm抽象类。
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .*;
import ;
import ;
import ;
import ;
import org.;
import org.;
import ;
import ;
public class ShiroRealm extends AuthorizingRealm {
private static Logger logger = ();
//这里尝试过使用@Autowired 但是发现会报错。这个是spring的注解。如果有知道原因的可以留言。谢谢
@Resource
private UserService userService;
@Resource
private SysRoleService sysRoleService;
@Resource
private SysAuthService authService;
/**
* 配置权限 注入权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
("--------权限配置-------");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser user = (SysUser) ();
try {
//注入角色(查询所有的角色注入控制器)
List<SysRole> list = (());
for (SysRole role: list){
(());
}
//注入角色所有权限(查询用户所有的权限注入控制器)
List<SysAuth> sysAuths = (());
for(SysAuth sysAuth:sysAuths){
(());
}
}catch (Exception e){
();
((e));
}
return authorizationInfo;
}
/**
* 用户验证
* @param token 账户数据
* @return
* @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号
String username = (String) ();
//这里需注意。看别人的教程有人是这样写的String password = (String) ();
//项目运行的时候报错,发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。
//源码:this(username, password != null ? () : null, false, null);
//不晓得是否是因为版本的原因,建议使用的时候下载源码进行查看
String password = new String((char[]) ());
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser user = (username);
if(null == user){
throw new UnknownAccountException();
}else {
if((())){
if(0 == ()){
throw new LockedAccountException();
}else if (2 == ()){
throw new DisabledAccountException();
}else{
SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,().toCharArray(),getName());
return authorizationInfo;
}
} else {
throw new IncorrectCredentialsException();
}
}
}
}
1.2 接下来配置Shiro的关键部分
这里要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
("--------------------shiro filter-------------------");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
(securityManager);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
//注意过滤器配置顺序 不能颠倒
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
// 配置不会被拦截的链接 顺序判断
("/static/**", "anon");
("/", "anon");
//拦截其他所以接口
("/**", "authc");
//配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
("/user/unlogin");
// 登录成功后要跳转的链接 自行处理。不用shiro进行跳转
// ("user/index");
//未授权界面;
("/user/unauth");
(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* shiro 用户数据注入
* @return
*/
@Bean
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
(shiroRealm());
return securityManager;
}
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
(true);
return advisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持 使用代理方式所以需要开启代码支持
* 一定要写入上面advisorAutoProxyCreator()自动代理。不然AOP注解不会生效
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
1.3 修改我们的Controller中的登录请求
// 这里如果不写method参数的话,默认支持所有请求,如果想缩小请求范围,还是要添加method来支持get, post等等某个请求。
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception {
BaseResponse<String> baseResponse = new BaseResponse<>();
Subject subject = ();
//数据库的密码我进行了Md5加密。如果没有进行加密的无需这个
((()));
UsernamePasswordToken token = new UsernamePasswordToken((),());
try {
(token);
//(getSession().getId());
(getSession().getId());
} catch (UnknownAccountException e){
("用户名不存在");
} catch (IncorrectCredentialsException e){
();
("密码错误");
} catch (LockedAccountException e){
(CodeField.ACCOUNT_NOT_ACTIVAT);
(CodeField.ACCOUNT_NOT_ACTIVAT_MSG);
}catch (DisabledAccountException e){
(CodeField.ACCOUNT_BAN);
(CodeField.ACCOUNT_BAN_MSG);
} catch (Exception e){
();
((e));
}
return baseResponse;
}
这里@RequestMapping之所以没加method是因为如果用户没登录,Shiro会调用get方法请求/login,而后面我们在login页面会用post请求发送form表单,所以这里就没设置method(默认支持所有请求)。
配置完成了就可以运行起来了。
1.4 开启接口权限
@RestController
@RequestMapping("user")
public class UserController(){
/**
* 测试
* @return
*/
@RequestMapping("/test")
//拥有此权限的才可以访问
@RequiresPermissions("user:test")
//拥有此角色的才可以访问
@RequiresRoles("admin")
public BaseResponse test() {
BaseResponse baseResponse = new BaseResponse();
("用户拥有该权限");
return baseResponse;
}
}
在进行权限校验的时候发现。当用户进行权限判定时。如果用户没有权限则会抛出UnauthorizedException异常,而如我们之前设置的那样进行跳转
("/user/unauth");
2.异常、缓存
经过查找发现定义的filter必须满足filter instanceof AuthorizationFilter,只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl设置后页面不跳转
解决方法要么就使用perms,roles,ssl,rest,port,要么自己配置异常处理,进行页面跳转。
这里选择自定义异常处理。处理全局异常。
2.1 自定义全局异常处理
import ;
import ;
import ;
import ;
import ;
import org.;
import org.;
import ;
import ;
import ;
import ;
import ;
import ;
/**
* Description: 全局异常处理
*
* @author zlp
* @create 2018-05-24 11:13
**/
public class GlobalExceptionResolver implements HandlerExceptionResolver {
private static Logger logger = ();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv;
//进行异常判断。如果捕获异常请求跳转。
if(ex instanceof UnauthorizedException){
mv = new ModelAndView("/user/unauth");
return mv;
}else {
mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
BaseResponse baseResponse = new BaseResponse();
("服务器异常");
();
((ex));
Map<String,Object> map = new HashMap<>();
String beanString = (baseResponse);
map = (beanString,);
(map);
(view);
return mv;
}
}
}
2.2 将自定义的异常处理注入Shiro中
在前面配置的ShiroConfig添加如下代码块
/**
* 注册全局异常处理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver(){
return new GlobalExceptionResolver();
}
如果我们登录之后多次访问的话,会发现权限验证会每次都执行一次。这是有问题的,因为像用户的权限这些我们提供给shiro一次就够了。所以我们进行缓存配置。前面已经引入了缓存依赖。所以我们直接贴代码
首先在项目配置包中写如缓存配置文件
<!--配置文件来源于网络。具体实际配置要参照配置文档。进行合理配置-->
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<diskStore path=""/>
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:
Ehcache的三种清空策略;
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 登录记录缓存锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
然后修改ShiroConfig
//添加方法
/**
* 开启缓存
* shiro-ehcache实现
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
("()");
EhCacheManager ehCacheManager = new EhCacheManager();
("classpath:");
return ehCacheManager;
}
//修改securityManager方法。
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
(shiroRealm());
//自定义缓存实现
(ehCacheManager());
return securityManager;
}
现在主流的缓存插件为Redis。但是我进行配置的时候总是会报数据源异常。因为网上用的连接池大部分都是阿里的druid。而我的项目使用的是springboot默认的连接池,配置不同。
3.自定义sessionManager
传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义ShiroSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,
import ;
import ;
import ;
import ;
import ;
import ;
import ;
/**
* Description:shiro框架 自定义session获取方式
* 可自定义session获取规则。这里采用ajax请求头authToken携带sessionId的方式
*
* @author zlp
* @create 2018-05-24 10:04
**/
public class ShiroSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "authToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSessionManager(){
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String id = (request).getHeader(AUTHORIZATION);
("id:"+id);
if((id)){
//如果没有携带id参数则按照父类的方式在cookie进行获取
("super:"+(request, response));
return (request, response);
}else{
//如果请求头中有 authToken 则其值为sessionId
(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,);
return id;
}
}
}
然后修改ShiroConfig 类。将自定义的ShiroSessionManager 注入管理器中
//添加bean
/**
* 自定义sessionManager
* @return
*/
@Bean
public SessionManager sessionManager(){
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
//这里可以不设置。Shiro有默认的session管理。如果缓存为Redis则需改用Redis的管理
(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}
//修改securityManager()方法
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
(shiroRealm());
//自定义session管理
(sessionManager());
//自定义缓存实现
(ehCacheManager());
return securityManager;
}