Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。相比较Spring Security,shiro有小巧、简单、易上手等的优点。所以很多框架都在使用shiro。而springboot作为一个开源框架,必然提供了和shiro整合的功能!接下来就用springboot整合shiro完成对于用户登录的判定和权限的验证.
公司项目采用的spring-boot框架。在做用户权限功能的时候准备采用shiro权限框架。前面也考虑过spring家族的spring security安全框架。但是经过网上查询对比最终选择了shiro。因为shiro含有基本的安全控制功能,并且配置更为简单,使用也更加简洁。
首先引入shiro依赖jar包
org.apache.shiro
shiro-spring
1.4.0
org.apache.shiro
shiro-ehcache
1.2.2
本次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; //权限描述
}
Realm是一个Dao,通过它来验证用户身份和权限。这里Shiro不做权限的管理工作,需要我们自己管理用户权限,只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。Shiro就会自动的进行权限判断。在项目包下建一个ShiroRealm类,继承AuthorizingRealm抽象类。
import com.lingjiugis.ocr.domain.SysAuth;
import com.lingjiugis.ocr.domain.SysRole;
import com.lingjiugis.ocr.domain.SysUser;
import com.lingjiugis.ocr.service.SysAuthService;
import com.lingjiugis.ocr.service.SysRoleService;
import com.lingjiugis.ocr.service.UserService;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.util.List;
public class ShiroRealm extends AuthorizingRealm {
private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
//这里尝试过使用@Autowired 但是发现会报错。这个是spring的注解。如果有知道原因的可以留言。谢谢
@Resource
private UserService userService;
@Resource
private SysRoleService sysRoleService;
@Resource
private SysAuthService authService;
/**
* 配置权限 注入权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
System.out.println("--------权限配置-------");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser user = (SysUser) principals.getPrimaryPrincipal();
try {
//注入角色(查询所有的角色注入控制器)
List list = sysRoleService.selectRoleByUser(user.getUserId());
for (SysRole role: list){
authorizationInfo.addRole(role.getSysRoleName());
}
//注入角色所有权限(查询用户所有的权限注入控制器)
List sysAuths = authService.queryByUserId(user.getUserId());
for(SysAuth sysAuth:sysAuths){
authorizationInfo.addStringPermission(sysAuth.getSysAuthPermission());
}
}catch (Exception e){
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
}
return authorizationInfo;
}
/**
* 用户验证
* @param token 账户数据
* @return
* @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号
String username = (String) token.getPrincipal();
//这里需注意。看别人的教程有人是这样写的String password = (String) token.getCredentials();
//项目运行的时候报错,发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。
//源码:this(username, password != null ? password.toCharArray() : null, false, null);
//不晓得是否是因为版本的原因,建议使用的时候下载源码进行查看
String password = new String((char[]) token.getCredentials());
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser user = userService.selectByAccount(username);
if(null == user){
throw new UnknownAccountException();
}else {
if(password.equals(user.getUserPassword())){
if(0 == user.getUserState()){
throw new LockedAccountException();
}else if (2 == user.getUserState()){
throw new DisabledAccountException();
}else{
SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,user.getUserPassword().toCharArray(),getName());
return authorizationInfo;
}
} else {
throw new IncorrectCredentialsException();
}
}
}
}
这里要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
import com.lingjiugis.ocr.config.GlobalExceptionResolver;
import com.lingjiugis.ocr.filter.ShiroSessionManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("--------------------shiro filter-------------------");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new LinkedHashMap<>();
//注意过滤器配置顺序 不能颠倒
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
//拦截其他所以接口
filterChainDefinitionMap.put("/**", "authc");
//配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/user/unlogin");
// 登录成功后要跳转的链接 自行处理。不用shiro进行跳转
// shiroFilterFactoryBean.setSuccessUrl("user/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* shiro 用户数据注入
* @return
*/
@Bean
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持 使用代理方式所以需要开启代码支持
* 一定要写入上面advisorAutoProxyCreator()自动代理。不然AOP注解不会生效
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
// 这里如果不写method参数的话,默认支持所有请求,如果想缩小请求范围,还是要添加method来支持get, post等等某个请求。
@RequestMapping("/login")
public String login(HttpServletRequest request, Map map) throws Exception {
BaseResponse baseResponse = new BaseResponse<>();
Subject subject = SecurityUtils.getSubject();
//数据库的密码我进行了Md5加密。如果没有进行加密的无需这个
user.setUserPassword(MD5Util.getPwd(user.getUserPassword()));
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserAccount(),user.getUserPassword());
try {
subject.login(token);
//System.out.println(getSession().getId());
baseResponse.success(getSession().getId());
} catch (UnknownAccountException e){
baseResponse.setMsg("用户名不存在");
} catch (IncorrectCredentialsException e){
e.printStackTrace();
baseResponse.setMsg("密码错误");
} catch (LockedAccountException e){
baseResponse.setCode(CodeField.ACCOUNT_NOT_ACTIVAT);
baseResponse.setMsg(CodeField.ACCOUNT_NOT_ACTIVAT_MSG);
}catch (DisabledAccountException e){
baseResponse.setCode(CodeField.ACCOUNT_BAN);
baseResponse.setMsg(CodeField.ACCOUNT_BAN_MSG);
} catch (Exception e){
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
}
return baseResponse;
}
这里@RequestMapping之所以没加method是因为如果用户没登录,Shiro会调用get方法请求/login,而后面我们在login页面会用post请求发送form表单,所以这里就没设置method(默认支持所有请求)。
配置完成了就可以运行起来了。
@RestController
@RequestMapping("user")
public class UserController(){
/**
* 测试
* @return
*/
@RequestMapping("/test")
//拥有此权限的才可以访问
@RequiresPermissions("user:test")
//拥有此角色的才可以访问
@RequiresRoles("admin")
public BaseResponse test() {
BaseResponse baseResponse = new BaseResponse();
baseResponse.setMsg("用户拥有该权限");
return baseResponse;
}
}
在进行权限校验的时候发现。当用户进行权限判定时。如果用户没有权限则会抛出UnauthorizedException异常,而如我们之前设置的那样进行跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
经过查找发现定义的filter必须满足filter instanceof AuthorizationFilter,只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl设置后页面不跳转
解决方法要么就使用perms,roles,ssl,rest,port,要么自己配置异常处理,进行页面跳转。
这里选择自定义异常处理。处理全局异常。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import com.lingjiugis.ocr.response.base.BaseResponse;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* Description: 全局异常处理
*
* @author zlp
* @create 2018-05-24 11:13
**/
public class GlobalExceptionResolver implements HandlerExceptionResolver {
private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class);
@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();
baseResponse.setMsg("服务器异常");
ex.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(ex));
Map map = new HashMap<>();
String beanString = JSON.toJSONString(baseResponse);
map = JSON.parseObject(beanString,Map.class);
view.setAttributesMap(map);
mv.setView(view);
return mv;
}
}
}
在前面配置的ShiroConfig添加如下代码块
/**
* 注册全局异常处理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver(){
return new GlobalExceptionResolver();
}
如果我们登录之后多次访问的话,会发现权限验证会每次都执行一次。这是有问题的,因为像用户的权限这些我们提供给shiro一次就够了。所以我们进行缓存配置。前面已经引入了缓存依赖。所以我们直接贴代码
首先在项目配置包中写如缓存配置文件ehcache-shiro.xml
然后修改ShiroConfig
//添加方法
/**
* 开启缓存
* shiro-ehcache实现
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
System.out.println("ShiroConfiguration.getEhCacheManager()");
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
//修改securityManager方法。
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//自定义缓存实现
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
现在主流的缓存插件为Redis。但是我进行配置的时候总是会报数据源异常。因为网上用的连接池大部分都是阿里的druid。而我的项目使用的是springboot默认的连接池,配置不同。
传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义ShiroSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 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 = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
System.out.println("id:"+id);
if(StringUtils.isEmpty(id)){
//如果没有携带id参数则按照父类的方式在cookie进行获取
System.out.println("super:"+super.getSessionId(request, response));
return super.getSessionId(request, response);
}else{
//如果请求头中有 authToken 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
return id;
}
}
}
然后修改ShiroConfig 类。将自定义的ShiroSessionManager 注入管理器中
//添加bean
/**
* 自定义sessionManager
* @return
*/
@Bean
public SessionManager sessionManager(){
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
//这里可以不设置。Shiro有默认的session管理。如果缓存为Redis则需改用Redis的管理
shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}
//修改securityManager()方法
/**
* 配置管理层。即安全控制层
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//自定义session管理
securityManager.setSessionManager(sessionManager());
//自定义缓存实现
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}