版权声明:作者原创,转载请注明出处。
本系列文章目录地址:http://blog.csdn.net/u011961421/article/details/79416510
Shiro是Apache的一个开源安全框架,旨在简化身份验证和授权,主要用来处理身份认证,授权,企业会话管理和加密等,并且Shiro不依赖任何容器在JavaSE和JavaEE项目中都可以使用。
相比较Spring家族的Spring Security,Shiro在保持强大功能的同时,还具有简单灵活轻量等特点,学习成本也要低很多,目前受到越来越多项目系统的应用,更多关于shiro的介绍可以自己百度,这里不偏题。
以下为SpringBoot集成Shiro,实现基础登陆认证和权限管理的过程。
一.引入依赖文件
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
注意有需要的可以同时引入shiro-ehcache实现缓存,我这边因为后面会集成使用redis,所以就不引入了。引入shiro-ehcache也并不复杂,多加一个配置可自行百度,对于分布式部署来说,没有必要使用shiro-ehcache。
二.基础表
这里介绍使用简单的用户+权限的表结构,需要三张表,用户表,角色表,以及关系表,建表sql如下
create table CMS_USER_INFO
(
ID integer(10) not null auto_increment comment '用户ID',
USER_CODE varchar(20) not null comment '用户编码',
USER_NAME varchar(64) not null comment '用户名称',
USER_PWD varchar(150) comment '用户密码',
REMARK varchar(60) comment '备注',
CREATE_BY varchar(20) comment '创建人',
CREATE_DATE datetime comment '创建时间',
MODIFIED_BY varchar(20) comment '修改人',
MODIFIED_DATE datetime comment '修改时间',
SORTNO integer(2) default 0 comment '排序',
STATE integer(2) comment '数据状态',
primary key (ID)
);
create table CMS_ROLE_INFO
(
ID integer(10) not null auto_increment comment '角色编号',
ROLE_CODE varchar(20) not null comment '角色编码',
ROLE_NAME varchar(60) not null comment '角色名称',
CREATE_BY varchar(20) comment '创建人',
CREATE_DATE datetime comment '创建时间',
MODIFIED_BY varchar(20) comment '修改人',
MODIFIED_DATE datetime comment '修改时间',
SORTNO integer(2) default 0 comment '排序',
STATE integer(2) comment '数据状态',
primary key (ID)
);
create table CMS_USER_ROLE_R
(
ID int not null auto_increment,
USER_CODE varchar(10),
ROLE_CODE varchar(10),
CREATE_BY varchar(20) comment '创建人',
CREATE_DATE datetime comment '创建时间',
MODIFIED_BY varchar(20) comment '修改人',
MODIFIED_DATE datetime comment '修改时间',
SORTNO integer(2) default 0 comment '排序',
STATE integer(2) comment '数据状态',
primary key (ID)
);
测试数据如下
INSERT INTO `cms_user_info` VALUES (1, 'admin', '管理员', 'd0970714757783e6cf17b26fb8e2298f', '测试备注', 'admin', '2017-12-8 20:32:02', 'admin', '2017-12-8 20:32:09', 0, 1);
INSERT INTO `cms_user_info` VALUES (2, '17040406', '张三', 'd0970714757783e6cf17b26fb8e2298f', '测试备注', 'admin', '2017-12-8 20:32:02', 'admin', '2017-12-8 20:32:09', 1, 1);
INSERT INTO `cms_role_info` VALUES (1, 'admin', '系统管理员', 'admin', '2017-12-8 19:06:35', 'admin', '2017-12-8 19:06:39', 0, 1);
INSERT INTO `cms_role_info` VALUES (2, 'guest', '客人', 'admin', '2017-12-8 19:06:35', 'admin', '2017-12-8 19:06:35', 1, 1);
INSERT INTO `cms_user_role_r` VALUES (1, 'admin', 'admin', 'admin', '2017-12-8 20:19:34', 'admin', '2017-12-8 20:19:34', 0, 1);
INSERT INTO `cms_user_role_r` VALUES (2, '17040406', 'guest', 'admin', '2017-12-8 20:19:34', 'admin', '2017-12-8 20:19:34', 0, 1);
三.配置shiro
SpringBoot中集成shiro时,因为省去了配置文件,所以需要编写配置类,使用@Configuration注解注入配置类。这里最简洁shiro的配置需要包含三个方法:
1.注册Realm至Spring Bean,Realm是认证和授权的具体实现,shiro对于使用者来说需要实现的一个方法,它的简洁之处也在于只需要实现这个接口便可使用shiro。
2.注册SecurityManager安全管理器,无特殊需求使用默认管理器便可,SecurityManager是shiro的主入口,该配置方法将用户自定义的Realm注入SecurityManager即可。
3.配置Filter访问策略,这里请看代码,相信详细注解一看就明白。如下:
package com.pf.org.cms.common;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
/**
* @Auther: pf
* @Date: 2017/12/12 19:34
* @Description: shiro配置组件
*/
@Configuration
public class ShiroConfiguration {
private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
/**
* 注入Realm
* @return MyRealm
*/
@Bean(name = "myRealm")
public MyRealm myAuthRealm() {
MyRealm myRealm = new MyRealm();
log.info("myRealm注册完成");
return myRealm;
}
/**
* 注入SecurityManager
* @param myRealm
* @return SecurityManager
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("myRealm")MyRealm myRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm);
log.info("securityManager注册完成");
return manager;
}
/**
* 注入Filter
* @param securityManager
* @return ShiroFilterFactoryBean
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
// 配置登录的url和登录成功的url
filterFactoryBean.setLoginUrl("/auth/login");
filterFactoryBean.setSuccessUrl("/home");
// 配置未授权跳转页面
filterFactoryBean.setUnauthorizedUrl("/errorPage/403");
// 配置访问权限
LinkedHashMap filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/css/**", "anon"); // 表示可以匿名访问
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/imgs/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/auth/**", "anon");
filterChainDefinitionMap.put("/errorPage/**", "anon");
filterChainDefinitionMap.put("/demo/**", "anon");
filterChainDefinitionMap.put("/swagger-*/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/admin/**", "roles[admin]");// 表示admin权限才可以访问,多个加引号用逗号相隔
filterChainDefinitionMap.put("/*", "authc");// 表示需要认证才可以访问
filterChainDefinitionMap.put("/**", "authc");
filterChainDefinitionMap.put("/*.*", "authc");
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("shiroFilter注册完成");
return filterFactoryBean;
}
}
注意shiro支持配置路径,页面标签,注解等等多种权限配置方式,和不同粒度的权限配置。
1.配置方式:
默认过滤器(10个)
anon -- org.apache.shiro.web.filter.authc.AnonymousFilter
authc -- org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic -- org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms -- org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port -- org.apache.shiro.web.filter.authz.PortFilter
rest -- org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles -- org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl -- org.apache.shiro.web.filter.authz.SslFilter
user -- org.apache.shiro.web.filter.authc.UserFilter
logout -- org.apache.shiro.web.filter.authc.LogoutFilter
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
2.注解方式
3.页面标签:博主使用较少,应用场景也比较少,可以自己百度,这里不做介绍。
四.编写MyRealm
正如第三点所说,因为shiro并不知道你登陆认证的具体逻辑和授权的具体逻辑,所以需要用户自己实现,继承AuthorizingRealm,实现doGetAuthorizationInfo(授权)和doGetAuthenticationInfo(登陆认证)两个抽象方法,具体代码如下:
package com.pf.org.cms.common;
import com.pf.org.cms.entity.UserInfo;
import com.pf.org.cms.entity.UserRoleInfo;
import com.pf.org.cms.service.UserService;
import com.pf.org.cms.utils.MD5Util;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Auther: pf
* @Date: 2017/12/12 19:29
* @Description: 认证和授权具体实现
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 为当前subject授权
* @param principalCollection
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Map params = new HashMap<>();
params.put("userCode", (String) super.getAvailablePrincipal(principalCollection));
List userRoleInfos = userService.getUserRoleInfos(params);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if(!userRoleInfos.isEmpty()) {
for(UserRoleInfo role : userRoleInfos) {
info.addRole(role.getRoleCode());
}
}
return info;
}
/**
* 认证登陆subject身份
* @param authenticationToken
* @return AuthenticationInfo
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Map params = new HashMap<>();
params.put("userCode", (String)authenticationToken.getPrincipal());
List userInfos = userService.getUserInfos(params);
if (userInfos.isEmpty()) {
throw new UnknownAccountException();
} else if(userInfos.size() > 1) {
throw new DisabledAccountException();
} else {
UserInfo user = userInfos.get(0);
// 校验密码
return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), user.getUserPwd(), ByteSource.Util.bytes("2w@W"), getName());
}
}
}
五.登陆和授权验证
下面是登陆的controller:
package com.pf.org.cms.web;
import com.pf.org.cms.common.IConstants;
import com.pf.org.cms.entity.JsonBean;
import com.pf.org.cms.utils.ParamUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @Auther: pf
* @Date: 2017/12/12 19:41
* @Description:
*/
@Controller
@RequestMapping(value = "/auth")
public class AuthenticationController {
private static final Logger log = LoggerFactory.getLogger(AuthenticationController.class);
@RequestMapping(value = "/login")
public String login() {
return "/login";
}
@ResponseBody
@RequestMapping(value = "/login_in", produces = "application/json;charset=UTF-8")
public JsonBean loginIn(HttpServletRequest request) {
JsonBean reJson = new JsonBean();
Map paramMap = ParamUtils.handleServletParameter(request);
String userCode = MapUtils.getString(paramMap, "userCode");
String userPwd = MapUtils.getString(paramMap, "userPwd");
// shiro认证
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userCode, userPwd);
try {
subject.login(token);
} catch (UnknownAccountException e) {
reJson.setMessage("账户不存在");
return reJson;
} catch (DisabledAccountException e) {
reJson.setMessage("账户存在问题");
return reJson;
} catch (AuthenticationException e) {
reJson.setMessage("密码错误");
return reJson;
} catch (Exception e) {
log.info("登陆异常", e);
reJson.setMessage("登陆异常");
return reJson;
}
reJson.setStatus(IConstants.RESULT_INT_SUCCESS);
String res = subject.getPrincipals().toString();
if (subject.hasRole("admin")) {
res = res + "----------你拥有admin权限";
}
if (subject.hasRole("guest")) {
res = res + "----------你拥有guest权限";
}
reJson.setData(res);
reJson.setMessage("登陆成功");
return reJson;
}
}
页面:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<#include "/common/common.ftl"/>
head>
<body>
<div>
<span>hello! please login!span>
div>
<div>
<span>用户名:span><input type="text" id="userCode" />
<span>密码:span><input type="text" id="userPwd" />
div>
<div>
<input type="button" onclick="submitLogin()" value="登陆">
div>
body>
html>
<script type="text/javascript" src="/js/md5.js">script>
<script type="text/javascript">
function submitLogin() {
var userCode = $.trim($("#userCode").val());
var userPwd = $.trim($("#userPwd").val());
var hexPwd = hex_md5(userPwd);
$.ajax({
type: 'POST',
url: "http://localhost:8080/auth/login_in",
data: {"userCode":userCode,"userPwd":hexPwd},
dataType:"json",
success: function(rep){
if(rep.status == 0){
alert(rep.message);
alert(rep.data);
//window.location.reload();
// loadPage(0,baseUrl);
}else{
alert(rep.message);
}
},
error:function(rep){
alert("获取信息失败!");
}
});
}
script>
编写完成后启动SpringBoot工程,访问不允许匿名访问的路径,会跳转到登陆页面,登陆成功后可查看自己所拥有的权限,同样可以访问相关路径测试是否授权成功。具体代码可以访问github(https://github.com/15651037763/cms)下载。
写在最后,这里介绍的只是最简单方式的使用,以上github地址为本系列文章项目地址,本人能力有限博客略有延迟,更多shiro功能可以关注github或持续更新,在学习和研究的同时,博客会做到尽量总结全面不遗漏。