前言
这里我才用了Spring Boot的方式创建的项目,所以和Spring xml的注解有所不同。但道理都差不多的。
因为用到了Redis。需要安装Redis的参照:Redis的安装和使用
gitee项目地址(数据库在根目录):https://gitee.com/benzhu/shiro_conversation
项目下载:本地下载
知识点
1.连接Redis用的的包
Spring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis依赖于spring-data-redis 和 lettuce 。Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成 Lettuce,但如果你从 Spring Boot 1.5.X 切换过来,几乎感受不大差异,这是因为 spring-boot-starter-data-redis 为我们隔离了其中的差异性。
Lettuce 是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀 netty NIO 框架来高效地管理多个连接。 如果注入失败的话记得查看是否导入的包名有错。我这里的案例是Spring2版本的。
注意不同版本的spring boot下,redis的starter依赖名略有不同,如果上面的不行,可以尝试spring-boot-starter-data-redis,spring-boot-starter-redis。
2.配置文件的问题
如果配置了连接池的空闲数啥的记得导入下面的包:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3.StringRedisTemplate与RedisTemplate区别点
为什么要特别拎着个出来呢,因为我存入的是byte[]数组,但是在用可视化工具打开Redis的时候发现里面是乱码的,然后尝试处理发现处理失败,程序还是能够正常使用的,就是看着有点不爽。
StringRedisTemplat两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
两者的关系是StringRedisTemplate继承RedisTemplate;其实他们两者之间的区别主要在于他们使用的序列化类: RedisTemplate使用的是JdkSerializationRedisSerializer存入数据会将数据先序列化成字节数组然后在存入Redis数据库。
StringRedisTemplate使用的是StringRedisSerializere。
教程
1.导入Maven
只给导入包的部分。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--shiro注解(aop生效) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
</dependencies>
2.配置文件
这里主要是添加Redis的配置;注意我安装Redis的时候我是没有配置密码的。
# 端口设置
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?&useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#开启aop
spring.aop.proxy-target-class=true
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
# 连接超时时间(毫秒)
#spring.redis.timeout=0
3.编写User实体类
package com.benzhu.shiro.domain;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
4.编写Shiro配置类
package com.benzhu.shiro.Filter;
import com.benzhu.shiro.realm.CustomRealm;
import com.benzhu.shiro.session.CustomSessionManager;
import com.benzhu.shiro.session.RedisSessionDao;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
@Resource(name = "realm")
private CustomRealm customRealm;
@Resource(name = "redisSessionDao")
private RedisSessionDao redisSessionDao;
@Bean(name="shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager manager) {
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
//配置登录的url和登录成功的url以及验证失败的url
bean.setLoginUrl("/login");
bean.setUnauthorizedUrl("/error");
//配置自定义的Filter
Map<String, Filter>filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("roleOrFilter", new RolesOrFilter());
bean.setFilters(filtersMap);
//配置访问权限
LinkedHashMap<String, String>filterChainDefinitionMap=new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/sublogin", "anon");
filterChainDefinitionMap.put("/usererror", "anon");
filterChainDefinitionMap.put("/roles1", "roles[admin]");
filterChainDefinitionMap.put("/roles2", "roles[admin,user]");
filterChainDefinitionMap.put("/permission1", "perms[user:update]");
filterChainDefinitionMap.put("/permission2", "perms[user:update,user:select]");
filterChainDefinitionMap.put("/roles3", "roleOrFilter[admin,user]");
filterChainDefinitionMap.put("/**","authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
//配置核心安全事务管理器
@Bean(name="securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("sessionManager")DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
//配置自定义Realm
manager.setRealm(customRealm);
//配置自定义sessionManager
manager.setSessionManager(sessionManager);
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //创建加密对象
matcher.setHashAlgorithmName("md5"); //加密的算法
matcher.setHashIterations(1);//加密次数
customRealm.setCredentialsMatcher(matcher); //放入自定义Realm
return manager;
}
//以下是开启注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
//配置session
@Bean(name="sessionManager")
public CustomSessionManager sessionManager(){
//把sessionManager注入Bean
CustomSessionManager manager = new CustomSessionManager();
manager.setSessionDAO(redisSessionDao);
return manager;
}
}
5.编写自定义的Realm
package com.benzhu.shiro.realm;
import com.benzhu.shiro.dao.UserDao;
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 org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Component("realm")
public class CustomRealm extends AuthorizingRealm {
@Resource
private UserDao userDao;
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
//从缓存中拿到角色数据(没有设置缓存只能再查一次数据库)
Set<String> roles = getRolesByUserName(username);
//从缓存中拿到权限数据(没有设置缓存只能再查一次数据库)
Set<String> permissions = getPermissionUserName(roles);
//返回对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
private Set<String> getPermissionUserName(Set<String> roles) {
Set<String> sets = new HashSet<>();
for (String role : roles){
List<String> permission = userDao.getPermissionByUserName(role);
for (String permis:permission) {
//这里输出可以看出运行了两次
System.out.println(permis);
sets.add(permis);
}
}
return sets;
}
private Set<String> getRolesByUserName(String username) {
List<String> roles = userDao.getRolesByUSerName(username);
Set<String> sets = new HashSet<>(roles);
return sets;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.从主体传过来的认证信息中获取用户名
String username = (String) token.getPrincipal();
//2.通过用户名到数据库中获取凭证
String password = getPasswordByUserName(username);
if(password == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,"customRealm");
//加盐验证
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("benzhu"));
return authenticationInfo;
}
private String getPasswordByUserName(String username) {
//模拟数据库查询
return userDao.getUserPassWord(username);
}
/* public static void main(String[] args) {
//计算加密结果的主方法 用来计算加密的密码结果 非必需
Md5Hash md5Hash = new Md5Hash("123456","benzhu");
System.out.println(md5Hash);
}*/
}
6.编写数据库访问的方法
dao/UserDao.java
package com.benzhu.shiro.dao;
import java.util.List;
public interface UserDao {
String getUserPassWord(String username);
List<String> getRolesByUSerName(String username);
List<String> getPermissionByUserName(String username);
}
dao/impl/UserDaoImpl.java
package com.benzhu.shiro.dao.impl;
import com.benzhu.shiro.dao.UserDao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@Component
public class UserDaoImpl implements UserDao {
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public String getUserPassWord(String username) {
String sql = "select password from users where username = ?";
List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>(){
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("password");
}
});
if(list==null){
return null;
}
return list.get(0);
}
@Override
public List<String> getRolesByUSerName(String username) {
String sql = "select role_name from user_roles where username = ?";
List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("role_name");
}
});
return list;
}
@Override
public List<String> getPermissionByUserName(String username) {
String sql = "select permission from roles_permissions where role_name = ?";
List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("permission");
}
});
return list;
}
}
7.编写自定义Shiro拦截方法
Filter/AuthorizationFilter.java
package com.benzhu.shiro.Filter;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class RolesOrFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest,servletResponse);
String[] roles = (String[]) o;
if(roles == null || roles.length == 0){
return false;
}
for (String role : roles){
if(subject.hasRole(role)){
return true;
}
}
return false;
}
}
8.编写controller
controller/UserController.java
package com.benzhu.shiro.controller;
import com.benzhu.shiro.domain.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller("UserController")
@RequestMapping("/")
public class UserController {
@RequestMapping(value = "login")
public String login(){
return "login";
}
@RequestMapping(value = "usererror")
public String error(){
return "usererror";
}
@RequestMapping(value = "sublogin")
public String sublogin(User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
try {
subject.login(token);
try {
subject.checkRole("admin");
subject.checkPermission("user:update");
//理解非注解也可以做转跳拦截之类的动作
if(subject.hasRole("admin")){
System.out.println("拥有admin角色!");
}
if(subject.isPermitted("user:update")){
System.out.println("拥有user:update权限!");
}
return "index";
}catch (UnauthorizedException exception){
System.out.println("角色授权或者权限授权失败!");
return "error";
}
}catch (AuthenticationException e){
System.out.println("认证失败!");
return "usererror";
}
}
@RequiresRoles("admin")
@RequestMapping("testroels")
@ResponseBody
public String testroels(){
return "有admin角色!";
}
@RequiresRoles("admin1")
@RequestMapping("testroels1")
@ResponseBody
public String testroels1(){
return "有admin1角色!";
}
@RequiresPermissions("user:update")
@RequestMapping("testPermission")
@ResponseBody
public String testPermission(){
return "拥有user:update权限";
}
@RequiresPermissions("user:update1")
@RequestMapping("testPermission1")
@ResponseBody
public String testPermission1(){
return "拥有user:update1权限";
}
@RequiresRoles("admin")
@RequestMapping("exit")
public String exit(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
@RequestMapping("roles1")
@ResponseBody
public String roles1(){
return "拥有admin角色";
}
@RequestMapping("roles2")
@ResponseBody
public String roles2(){
return "拥有admin和user角色";
}
@RequestMapping("permission1")
@ResponseBody
public String permission1(){
return "拥有user:update权限";
}
@RequestMapping("permission2")
@ResponseBody
public String permission2(){
return "拥有user:update和user:select权限";
}
@RequestMapping("roles3")
@ResponseBody
public String roles3(){
return "拥有admin或者user角色";
}
}
9.编写前端页面
这里自己参照controller写一下就好。还要多写一个error页面。
10.编写seesion的缓存在Redis
本文的重点;对Shiro中的Session保存在Redis;对Redis进行增删查改。
session/CustomSessionManager.java
package com.benzhu.shiro.session;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
//判断sessionKey是不是WebSessionKey是的话才能强转
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request != null && sessionId !=null){
//如果request不为空获取request的session
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
//为空就再获取一次
Session session = super.retrieveSession(sessionKey);
if(request != null && sessionId != null){
//获取request的session
request.setAttribute(sessionId.toString(),session);
}
return session;
}
}
config/RedisConfig.java
package com.benzhu.shiro.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisConfig {
@Autowired
private RedisTemplate redisTemplate;
public void set(byte[] key, byte[] value) {
//插入session值
redisTemplate.opsForValue().set(key, value);
//stringRedisTemplate.opsForValue().set(new String(key), new String(value), i, TimeUnit.SECONDS);
}
public void expire(byte[] key, int i) {
//设置超时时间
//三个参数 分别是key 时间 时间类型(时分秒等)
redisTemplate.expire(key, i, TimeUnit.SECONDS);
}
public byte[] get(byte[] key) {
//获取session值
return (byte[]) redisTemplate.opsForValue().get(key);
}
public void del(byte[] key) {
//删除session值
redisTemplate.delete(key);
}
public Set<byte[]> keys(String shiro_session_prefix) {
return redisTemplate.keys(shiro_session_prefix + "*");
}
}
11.重写写Shiro中的Session提高Redis的读取效率
就是重写Shiro的方法把session放到request里面减少读取Redis的次数来提高效率。
session/CustomSessionManager.java
package com.benzhu.shiro.session;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
//判断sessionKey是不是WebSessionKey是的话才能强转
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request != null && sessionId !=null){
//如果request不为空获取request的session
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
//为空就再获取一次
Session session = super.retrieveSession(sessionKey);
if(request != null && sessionId != null){
//获取request的session
request.setAttribute(sessionId.toString(),session);
}
return session;
}
}
12.启动测试
在代码里面也写了一些输出来验证读取的次数是减少了,效果图就不给了,很多种验证的方法。
评论