一、简介
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
二 、架构思路
从上到下依次说明
1.subject :主体,主体可以是很多,用户或者程序,但是要访问系统必须进行验证和授权。
2.securityManager:安全管理器,里面包括授权和认证的。
3.authenticator:认证器,主体进行认证最终通过authenticator进行的。
4.authorizer:授权器,主体进行授权最终通过authorizer进行的。
5.sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。可以实现单点登录。
6.SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
7.cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
8.realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。(它的主要目的是与数据库打交道,查询数据库中的认证的信息(比如用户名和密码),查询授权的信息(比如权限的code等,所以这里可以理解为调用数据库查询一系列的信息,一般情况下在项目中采用自定义的realm,因为不同的业务需求不一样))
三、上手
shiro官网,个版本依赖都要自己选择
1.项目依赖,其他例如mysql的依赖就没有贴
<!-- springBoot的依赖,直接全家桶简单 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
/**
* 这个依赖主要包括的jar包如下,如果不用springboot,自己用spring
* org.apache.shiro:shiro-core:1.4.0
* org.apache.shiro:shiro-lang:1.4.0
* org.apache.shiro:shiro-cache:1.4.0
* org.apache.shiro:shiro-crypto-hash:1.4.0
* org.apache.shiro:shiro-crypto-cipher:1.4.0
* org.apache.shiro:shiro-config-core:1.4.0
* org.apache.shiro:shiro-config-ogdl:1.4.0
* org.apache.shiro:shiro-event:1.4.0
* org.apache.shiro:shiro-web:1.4.0
*/
<!-- spirng的依赖,boot里面也可以这么用 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro和redis集群的依赖,主要是用来做缓存的,不用缓存可不加 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
2.bean的注册
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 这个主要是配置ShiroFilter(通过此filter的配置实现对请求资源的过滤,哪些请求要放行,哪些要认证),用的工厂模式
* 需要有安全认证 securityManager下面配置
*/
//动态开关
@Value("${com.git.open-shiro}")
private boolean openShiro=true;
@Bean
public ShiroFilterFactoryBean ceateShirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设定的登录的路径
shiroFilterFactoryBean.setLoginUrl("/login");
// 登陆成功要跳转的页面,楼主用的前端控制配不配之无所谓
shiroFilterFactoryBean.setSuccessUrl("/index");
// 没有权限直接跳到登录页,按需求配置
shiroFilterFactoryBean.setUnauthorizedUrl("/login");
// 拦截器拦截的路径
Map<String, String> map = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
//map .put("/static/**", "anon");//楼主直接配static发现失效,没办法才将下面的目录全部在配一遍
map .put("/css/**", "anon");//允许访问就是anon
map .put("/editor/**", "anon");
map .put("/file/**", "anon");
map .put("/img/**", "anon");
map .put("/js/**", "anon");
map .put("/openUi/**", "anon");
map .put("/Public/**", "anon");
map .put("/roboto/**", "anon");
map .put("/login", "anon");
map .put("/login/login", "anon");//这个一定要记得配置,这就是登录的方法,不配置无限重定向
// 退出,不用自己实现
map .put("/logout", "logout");
//配置需要权限的路径
if(openShiro){
map .put("/**", "authc");
}else{
map .put("/**", "anon");
}
//需要权限就是authc
shiroFilterFactoryBean.setFilterChainDefinitionMap(map );
return shiroFilterFactoryBean;
}
/**
* 配置shiro的SecurityManager对象,返回的是DefaultWebSecurityManager,这里面需要realm,
* realm用的是自己的
*/
@Bean
public SessionsSecurityManager createSecurityManager(
AuthorizingRealm realm){
DefaultWebSecurityManager sManager =
new DefaultWebSecurityManager();
//通过realm访问数据库
sManager.setRealm(realm);
return sManager;
}
//shiro声明周期,注意注意,如果去前面用了@value注解读取yml里面的配置,就得把这个方法设置为static
@Bean("lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor newLifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@DependsOn(value="lifecycleBeanPostProcessor")
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。这个bug是参考谋篇博客的,忘记作者了,以前
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
// 启用shiro注解
@Bean
public AuthorizationAttributeSourceAdvisor newAuthorizationAttributeSourceAdvisor(
SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor bean = new AuthorizationAttributeSourceAdvisor();
bean.setSecurityManager(securityManager);
return bean;
}
//加密。这里就不配了
}
3.自定义的realm
@Service
public class ShiroUserRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(ShiroUserRealm.class);
@Autowired
private UserService userService;
@Autowired
private MenuService menuService;
/**授权方法,因为没有配置缓存,所以每次请求都会执行该方法*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("开始授权");
User user = (User) principals.getPrimaryPrincipal();
//获得权限的方法,说一下思路user-》userRole-》role-》roleMenu-》menu等下上这几个表的思路,可以直接一步怼sql也可以单表慢慢查,方法未贴出来
List<Menu> menuList =menuService.getUserAllMenu(user);
if(menuList==null||menuList.size()<=0)return null;
//获得权限的set集合,这里提前排除,不然会报下面的异常
Set<String> set = menuList.stream().map(Menu::getPermisson).filter(StringUtils::isNotBlank).collect(Collectors.toSet());
SimpleAuthorizationInfo info= new SimpleAuthorizationInfo();
info.setStringPermissions(set);
return info;
/**java.lang.IllegalArgumentException: Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.
* at org.apache.shiro.authz.permission.WildcardPermission.setParts(WildcardPermission.java:155),set有空值*/
}
/**认证方法,登录的时候执行的*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("开始认证");
//1.获取用户身份对象(例如用户名)
String username =(String) token.getPrincipal();
//2.基于用户名从数据库查询记录,基于mybatisPlus
User user=userService.getOne(new QueryWrapper<User>().eq("username",username));
//3.对查询结果进行验证,用户不存在则抛出异常
if(user==null) throw new AuthenticationException("用户不存在");
if(user.getStatus()==0)throw new AuthenticationException("用户已被禁用");
//4.对数据库查询出的相关信息进行封装
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(
user,//principal (身份对象)
user.getPassword(), //hashedCredentials(已加密的密码)
//credentialsSalt,//credentialsSalt (盐),这里暂时不想用盐所以就传null了
null,
this.getName());//realm name
//5.返回封装结果(传递给认证管理器)
return info;
}
@Override
public void setCredentialsMatcher(
CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher cMatcher=
new HashedCredentialsMatcher();
cMatcher.setHashAlgorithmName("MD5");
//设置加密的次数(这个次数应该与保存密码时那个加密次数一致),这里设置一下这样就不用自己传密码的时候加密了
cMatcher.setHashIterations(1);
super.setCredentialsMatcher(cMatcher);
}
}
4.登录方法
@PostMapping("/login")
@ResponseBody
public ApiResult login(String username, String password, String code, Boolean rememberMe) {
Subject subject= SecurityUtils.getSubject();
//2.提交用户信息
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
subject.login(token);
return ResultUtils.buildSucessObject(null);
}
5.加权限注解
@RequiresPermissions(value="user:read")//楼主这次在controller里面加的
四、演示
//控制台日志
2019-07-05 11:56:12,663 INFO (ShiroUserRealm.java:56)- 开始认证
username=张三
。。。
2019-07-05 11:56:14,639 INFO (ShiroUserRealm.java:40)- 开始授权
没权限的时候直接报错
没有权限后台的异常
java.lang.IllegalArgumentException: Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.
加了权限,正常显示
5.数据表结构
//用户表
@ApiModelProperty(value="用户ID")
@TableId(value="user_id", type= IdType.AUTO)
private Long userId;
/**
* 用户名
*/
@ApiModelProperty(value="用户名")
@TableId(value="username")
private String username;
//用户角色表
@ApiModelProperty(value="用户ID")
@TableField("user_id")
private Long userId;
/**
* 角色ID
*/
@ApiModelProperty(value="角色ID")
@TableField("role_id")
private Long roleId;
//角色表
@ApiModelProperty(value="角色ID")
@TableId(value="role_id", type= IdType.AUTO)
private Long roleId;
/**
* 角色名称
*/
@ApiModelProperty(value="角色名称")
@TableField("role_name")
private String roleName;
//角色菜单表
@ApiModelProperty(value="角色ID")
@TableField("role_id")
private Long roleId;
/**
* 菜单/按钮ID
*/
@ApiModelProperty(value="菜单/按钮ID")
@TableField("menu_id")
private Long menuId;
//菜单表
@ApiModelProperty(value="菜单/按钮ID")
@TableId(value="menu_id", type= IdType.AUTO)
private Long menuId;
/**
* 上级菜单ID
*/
@ApiModelProperty(value="上级菜单ID")
@TableField("parent_id")
private Long parentId;
/**
* 菜单/按钮名称
*/
@ApiModelProperty(value="菜单/按钮名称")
@TableField("menu_name")
private String menuName;
/**
* 菜单URL
*/
@ApiModelProperty(value="菜单URL")
@TableField("url")
private String url;
/**
* 权限标识
*/
@ApiModelProperty(value="权限标识")
@TableField("permisson")
private String permisson;
/**
* 类型 0菜单 1按钮
*/
@ApiModelProperty(value="类型 0菜单 1按钮")
@TableField("type")
private String type;
/**
* 排序
*/
@ApiModelProperty(value="排序")
@TableField("sort")
private Long sort;
6.总结
shiro的实现原理其实很简单,我这用着更简单,主要是配置一些bean,和自己的realm,其原理过滤器+拦截器+aop,下个版本可能会自己模拟实现授权和认证的代码(基于全注解模式)。
未实现配置:
1.缓存,下次楼主在会在使用redis实现shiro的缓存功能
2.web页面,楼主这次用的是 thymeleaf 模板,空也会在页面使用一下shiro的标签。
楼主邮箱 [email protected]
qq 2519946973
有疑问直接邮箱,原创文章,转发请注明出处。
7.后续
本文参考过以下文章 :
1.shiro官网 各版本依赖
2.小虾米的java梦,参考内容 原理解释部门
3.解决controller层的注解路径不拦截问题。这篇文章忘记连接了,抱歉!