网上很多介绍springboot+shiro+redis解决session共享的文章,但大部分都是基于springboot1.5版本的,有些接口发生变化。
本篇文章主要记录在SpringBoot2.x的基础上整合Shiro和Redis,实现权限控制与支持集群下的Session共享。
项目是基于Maven的,首先在pom.xml添加以下文件:
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
UTF-8
UTF-8
1.8
1.4.0
3.1.0
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-thymeleaf
org.apache.shiro
shiro-spring
${shiro-spring.version}
org.crazycake
shiro-redis
${shiro-redis.version}
org.springframework.session
spring-session-data-redis
commons-codec
commons-codec
利用第三方开源框架shiro-redis框架。
以下是数据库设计,是根据实体类自动生成,无需手动创建:
在application.properties文件添加以下内容:
server.port=8090
###datasource
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql=true
#Redis
spring.redis.host=10.1.44.13
spring.redis.port=6379
spring.redis.password=
spring.redis.jedis.pool.max-wait=61000ms
spring.redis.jedis.pool.max-active=1000
#session过期时间:秒
shiro.session.expire=1800
下面是实体类:
//用户实体
@Data
@Entity
@Table
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(unique = true, length = 32)
private String uid;// 用户id;
@Column(unique = true, length = 30)
private String username;// 登录账号.
@Column(length = 30)
private String name;// 名称
@Column(length = 11)
private String mobile;
@Column(length = 32)
private String password; // 密码;
@Column(length = 32)
private String salt;// 加密密码的盐
@Column(columnDefinition="int default 1",length = 1)
private int state;// 用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
@Transient
private List roles;
@Transient
private List permissions;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
/**
* 密码盐.
*/
public String getCredentialsSalt() {
return this.username + this.salt;
}
public SysUser(String username, String name, String mobile, String password) {
super();
this.username = username;
this.name = name;
this.mobile = mobile;
this.password = password;
}
public SysUser() {
super();
}
}
//角色实体
@Data
@Entity
@Table
public class SysRole implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(unique=true, length=32)
private String roleCode;//程序中判断使用,如"admin",这个是唯一的
@Column(unique=true, length=30)
private String roleName; // 角色名字
@Column(length=200)
private String description; //角色描述,UI界面显示使用
private Boolean state = Boolean.TRUE; //是否可用,如果不可用将不会添加给用户
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
//权限实体
@Data
@Entity
@Table
public class SysPermission implements Serializable{
private static final long serialVersionUID = 960801694129036736L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String PermName;//名称.
@Column(columnDefinition="enum('menu','button')",length=10)
private String resourceType;//资源类型,[menu|button]
@Column(length=200)
private String url;//资源路径.
@Column(length=100)
private String icon;
private int sort;
@Column(unique=true,length=20)
private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private int parentId; //父编号
private Boolean state = Boolean.TRUE;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
//用户角色关联实体
@Data
@Entity
@Table
public class SysUserRole implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String uid;
@Column(length=32)
private String roleCode;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
//角色权限关联实体
@Data
@Entity
@Table
public class SysRolePermission implements Serializable {
private static final long serialVersionUID = 8949842330879809712L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(length=32)
private String roleCode;
private int permissionId;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
配置持久类:
public interface SysUserDao extends CrudRepository{
/**通过username查找用户信息;*/
public SysUser findByUsername(String username);
}
public interface SysRoleDao extends JpaRepository,JpaSpecificationExecutor {
}
public interface SysPermissionDao extends JpaRepository,JpaSpecificationExecutor {
}
3.1 配置ShiroConfig
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.util.StringUtils;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Value("${shiro.session.expire}")
private int expire;
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器.
Map filterChainDefinitionMap = new LinkedHashMap();
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
//过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setRealm(myShiroRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 身份认证realm;
* (这个需要自己写,账号密码校验;权限等)
* @return
*/
@Bean
public ShiroRealm myShiroRealm(){
ShiroRealm myShiroRealm = new ShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());;
return myShiroRealm;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setExpire(expire);//缓存过期时间:秒
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
redisManager.setTimeout(30000);//连接redis超时
if(!StringUtils._isEmpty_(redisPassword))
redisManager.setPassword(redisPassword);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setExpire(expire);//session会话过期时间,默认就是1800秒
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
3.2配置ShiroRealm
import javax.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 cn.grgpay.entity.SysPermission;
import cn.grgpay.entity.SysRole;
import cn.grgpay.entity.SysUser;
import cn.grgpay.service.SysUserService;
/**
* 身份校验核心类;
* @version v.0.1
*/
public class ShiroRealm extends AuthorizingRealm{
@Resource
private SysUserService userInfoService;
/**
* 认证信息.(身份验证)
* :
* Authentication 是用来验证用户身份
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String)token.getPrincipal();
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser userInfo = userInfoService.findByUsername(username);
if(userInfo == null){
return null;
}
//加密方式;
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户名
userInfo.getPassword(), //密码
ByteSource.Util._bytes_(userInfo.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
/**
* 此方法调用 hasRole,hasPermission的时候才会进行回调.
* 权限信息.(授权):
* 1、如果用户正常退出,缓存自动清空;
* 2、如果用户非正常退出,缓存自动清空;
* 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
* (需要手动编程进行实现;放在service进行调用)
* 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
* 调用clearCached方法;
* :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser userInfo = (SysUser)principals.getPrimaryPrincipal();
for(SysRole role:userInfo.getRoles()){
authorizationInfo.addRole(role.getRoleCode());
}
for(SysPermission p:userInfo.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission());
}
return authorizationInfo;
}
}
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index(){
return "/index";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
}
// 登录提交地址和applicationontext-shiro.xml配置的loginurl一致。 (配置文件方式的说法)
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(HttpServletRequest request, Map map) throws Exception {
System.out.println("HomeController.login()");
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("UnknownAccountException -- > 账号不存在:");
msg = "UnknownAccountException -- > 账号不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密码不正确:");
msg = "IncorrectCredentialsException -- > 密码不正确:";
} else if("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 验证码错误");
msg = "kaptchaValidateFailed -- > 验证码错误";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不处理登录成功,由shiro进行处理.
return "/login";
}
}
@Controller
@RequestMapping("/user")
public class UserInfoController {
@Value("${server.port}")
private int port;
@Autowired
private SysUserService sysUserService;
/**
* 用户添加
* **@return**
*/
@PostMapping("/add")
@RequiresPermissions("user:add")//权限管理;
public String userInfoAdd(HttpServletRequest request,String username,String name,String mobile,String password){
sysUserService.add(new SysUser(username, name, mobile, password));
return "userInfoAdd";
}
/**
* 用户删除
* **@return**
*/
@RequestMapping("/del")
@RequiresPermissions("user:del")//权限管理;
public String userDel(HttpServletRequest request){
System.out.println(port+":从session拿出:"+request.getSession().getAttribute("calonUser"));
return "userInfoDel";
}
}
启动程序后表自动生成,然后执行以下sql语句,往数据库插入用户,角色,权限等初始化数据到数据库。
BEGIN;
INSERT INTO `sys_permission` VALUES (1, NULL, NULL, '用户管理', NULL, 0, 'user', 'menu', 0, b'1', '/user');
INSERT INTO `sys_permission` VALUES (2, NULL, NULL, '用户列表', NULL, 1, 'user:view', 'menu', 1, b'1', '/user/list');
INSERT INTO `sys_permission` VALUES (3, NULL, NULL, '添加用户', NULL, 1, 'user:add', 'menu', 2, b'1', '/user/add');
INSERT INTO `sys_permission` VALUES (4, NULL, NULL, '删除用户', NULL, 1, 'user:del', 'menu', 3, b'1', '/user/del');
INSERT INTO `sys_permission` VALUES (5, NULL, NULL, '修改用户', NULL, 1, 'user:update', 'menu', 4, b'1', '/user/update');
COMMIT;
BEGIN;
INSERT INTO `sys_role` VALUES (1, NULL, NULL, NULL, '管理员', b'1', 'admin');
INSERT INTO `sys_role` VALUES (2, NULL, NULL, NULL, '经理', b'1', 'manager');
INSERT INTO `sys_role` VALUES (3, NULL, NULL, NULL, '职员', b'1', 'employee');
COMMIT;
BEGIN;
INSERT INTO `sys_role_permission` VALUES (1, NULL, NULL, 1, 'admin');
INSERT INTO `sys_role_permission` VALUES (2, NULL, NULL, 2, 'admin');
INSERT INTO `sys_role_permission` VALUES (3, NULL, NULL, 3, 'admin');
INSERT INTO `sys_role_permission` VALUES (4, NULL, NULL, 4, 'admin');
INSERT INTO `sys_role_permission` VALUES (5, NULL, NULL, 5, 'admin');
COMMIT;
BEGIN;
INSERT INTO `sys_user` VALUES (1, NULL, NULL, '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1, 'admin', NULL, 'admin');
INSERT INTO `sys_user` VALUES (5, NULL, '13800138000', '测试人员', 'ba62e27376828480aeb0e320ade28310', '7FEF647A20958034', 0, 'B9FB25A2536EBE85D29EA00345B5E183', NULL, 'calon');
COMMIT;
BEGIN;
INSERT INTO `sys_user_role` VALUES (1, NULL, NULL, 'admin', 'admin');
COMMIT;
直接访问http://localhost:8080/user/add
正常情况会跳转到登录页面。用户名:admin,密码:123456