网上很多介绍springboot+shiro+redis解决session共享的文章,但大部分都是基于springboot1.5版本的,有些接口发生变化。
本篇文章主要记录在SpringBoot2.x的基础上整合Shiro和Redis,实现权限控制与支持集群下的Session共享。
1、项目依赖
项目是基于Maven的,首先在pom.xml添加以下文件:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<shiro-spring.version>1.4.0</shiro-spring.version>
<shiro-redis.version>3.1.0</shiro-redis.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
</dependencies>
利用第三方开源框架shiro-redis框架。
2、数据库设计
以下是数据库设计,是根据实体类自动生成,无需手动创建:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wbwRWN3y-1650937284913)(http://itechor.top/solo/upload/72ce6085cdf0464bbc93e907d96302fd_image.png)]
在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、Shiro配置
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;
}
}
4、页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YernZpxN-1650937284914)(http://itechor.top/solo/upload/b6d9901ddf3045b2a18b303a417023c4_image.png)]5、Controller
@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
6