任何一个企业级系统,权限必不可少
早年写的关于shiro(基本上是基于SSM框架(即Spring+SpringMVC+MyBatis)文章如下(仅供参考):
shiro实战系列\(一\)之入门实战
Spring\(二\)之入门示例
shiro实战系列\(二\)之入门实战续
shiro实战系列\(三\)之架构
shiro实战系列\(四\)之配置
shiro实战系列\(五\)之Authentication\(身份验证\)
shiro实战系列\(六\)之Authorization\(授权\)
shiro实战系列\(七\)之Realm
shiro实战系列\(八\)之安全管理器
shiro实战系列\(九\)之Web
shiro实战系列\(十\)之Subject
shiro实战系列\(十一\)之Caching
shiro实战系列\(十二\)之常用专业术语
shiro实战系列\(十三\)之单元测试
shiro实战系列\(十四\)之配置
shiro实战系列\(十五\)之Spring集成Shiro
上面一共十五篇文章是早年在创业公司做相关的技术调研整理而成的,代码例子较少,偏理论性比较强,所以本篇文章不再赘述一些理论性内容,接下来开始进入实战。
一、导入Maven依赖
这里列举的是子模块pom.xml
1.8
1.1.13
org.projectlombok
lombok
1.16.20
provided
com.baomidou
mybatis-plus-boot-starter
3.4.1
com.baomidou
mybatis-plus-extension
3.4.1
com.alibaba
druid-spring-boot-starter
${druid-spring-boot-starter.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
mysql
mysql-connector-java
8.0.19
org.springframework.boot
spring-boot-starter-data-redis-reactive
org.apache.shiro
shiro-spring
1.4.0
org.crazycake
shiro-redis
3.1.0
org.apache.commons
commons-lang3
3.5
com.alibaba
fastjson
1.2.47
redis.clients
jedis
2.9.0
org.springframework.boot
spring-boot-maven-plugin
repackage
父pom.xml(主要针对SpringBoot版本):
org.springframework.boot
spring-boot-starter-parent
2.1.5.RELEASE
版本一定要对,否则会有各种奇葩问题。
例如(如果版本不对会出现这样的问题,启动正常,在请求登录接口就会报这样的错误):
错误信息:
java.lang.NoSuchMethodError: redis.clients.jedis.ScanResult.getStringCursor()...
二、配置(application.yml)
server:
port: 5050
spring:
# Redis数据源
redis:
host: localhost
port: 6379
timeout: 6000
password: 123456
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
# 配置数据源 datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/wordpress?useUnicode=true&characterEncoding=utf-8&serverTimeZone=GMT
username: root
password: 1234
type: com.alibaba.druid.pool.DruidDataSource
# mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: auto
#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"
field-strategy: NOT_EMPTY
#数据库类型
db-type: MYSQL
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
三、编写Shiro核心配置类
核心配置类特别注意的是接口放行,否则访问接口会出现404。
package com.blog.tutorial07.shiro.config;
import com.blog.tutorial07.shiro.shiro.ShiroRealm;
import com.blog.tutorial07.shiro.shiro.ShiroSessionIdGenerator;
import com.blog.tutorial07.shiro.shiro.ShiroSessionManager;
import com.blog.tutorial07.shiro.utils.SHA256Util;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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 java.util.LinkedHashMap;
import java.util.Map;
/**
* @Description Shiro配置类
* @Author youcong
*/@Configuration
public class ShiroConfig {
private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
private final int EXPIRE = 1800;
//Redis配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
/**
* 开启Shiro-aop注解支持 * @Attention 使用代理方式所以需要开启代码支持
* @Author youcong
*/ @Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro基础配置 * @Author youcong
*/ @Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接 filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/user/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理器 * @Author youcong
*/ @Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义Ssession管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(shiroRealm());
return securityManager;
}
/**
* 身份验证器 * @Author youcong
*/ @Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 凭证匹配器 * 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置 * @Author youcong
*/ @Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}
/**
* 配置Redis管理器 * @Attention 使用的是shiro-redis开源插件
* @Author youcong
*/ @Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* 配置Cache管理器 * 用于往Redis存储权限和角色标识 * @Attention 使用的是shiro-redis开源插件
* @Author youcong
*/ @Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("id");
return redisCacheManager;
}
/**
* SessionID生成器 * @Author youcong
*/ @Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
return new ShiroSessionIdGenerator();
}
/**
* 配置RedisSessionDAO * @Attention 使用的是shiro-redis开源插件
* @Author youcong
*/ @Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(EXPIRE);
return redisSessionDAO;
}
/**
* 配置Session管理器 * @Author youcong
*/ @Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}
}
四、编写核心配置类中涉及的相关类
ShiroRealm.java
package com.blog.tutorial07.shiro.shiro;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blog.tutorial07.shiro.entity.Usermeta;
import com.blog.tutorial07.shiro.entity.Users;
import com.blog.tutorial07.shiro.service.UsermetaService;
import com.blog.tutorial07.shiro.service.UsersService;
import com.blog.tutorial07.shiro.utils.ShiroUtils;
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.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Description Shiro权限匹配和账号密码匹配
* @Author Sans
* @CreateTime 2019/6/15 11:27
*/public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UsersService sysUserService;
@Autowired
private UsermetaService sysRoleService;
/**
* 授权权限 * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中 * * @Author Sans
* @CreateTime 2019/6/12 11:44
*/ @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户ID
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Users user = (Users) principalCollection.getPrimaryPrincipal();
Long userId = user.getId();
//这里可以进行授权和处理
Set rolesSet = new HashSet<>();
QueryWrapper roleWrapper = new QueryWrapper<>();
roleWrapper.eq("user_id", user.getId());
roleWrapper.eq("meta_key", "wp_user_level");
List roleList = sysRoleService.list(roleWrapper);
for (Usermeta role : roleList) {
rolesSet.add(role.getMetaValue());
}
authorizationInfo.setRoles(rolesSet);
return authorizationInfo;
}
/**
* 身份认证 * * @Author Sans
* @CreateTime 2019/6/12 12:36
*/ @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) authenticationToken.getPrincipal();
//通过username从数据库中查找 User对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("user_login", username);
Users user = sysUserService.getOne(wrapper);
//判断账号是否存在
if (user == null) {
throw new AuthenticationException();
}
//判断账号是否被冻结
if (user.getUserStatus() == null || user.getUserStatus().equals("1")) {
throw new LockedAccountException();
}
//进行验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户名
user.getUserPass(), //密码
ByteSource.Util.bytes(user.getUserActivationKey()), //设置盐值
getName()
);
//验证成功开始踢人(清除缓存和Session)
ShiroUtils.deleteCache(username, true);
return authenticationInfo;
}
}
ShiroSessionIdGenerator.java
package com.blog.tutorial07.shiro.shiro;
import com.blog.tutorial07.shiro.constant.RedisConstant;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import java.io.Serializable;
/**
* @Description 自定义SessionId生成器
* @Author youcong
*/public class ShiroSessionIdGenerator implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
return String.format(RedisConstant.REDIS_PREFIX_LOGIN, sessionId);
}
}
ShiroSessionManager.java
package com.blog.tutorial07.shiro.shiro;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* @Description 自定义获取Token
* @Author youcong
*/public class ShiroSessionManager extends DefaultWebSessionManager {
//定义常量
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
//重写构造器
public ShiroSessionManager() {
super();
this.setDeleteInvalidSessions(true);
}
/**
* 重写方法实现从请求头获取Token便于接口统一 * 每次请求进来,Shiro会去从请求头找Authorization这个key对应的Value(Token) * @Author youcong
*/ @Override
public Serializable getSessionId(ServletRequest request, ServletResponse response) {
String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中存在token 则从请求头中获取token
if (!StringUtils.isEmpty(token)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
// 这里禁用掉Cookie获取方式
// 按默认规则从Cookie取Token // return super.getSessionId(request, response); return null;
}
}
}
五、编写相关工具类
SHA256Util.java
package com.blog.tutorial07.shiro.utils;
import org.apache.shiro.crypto.hash.SimpleHash;
/**
* @Description Sha-256加密工具
* @Author youcong
*/public class SHA256Util {
/** 私有构造器 **/
private SHA256Util(){};
/** 加密算法 **/
public final static String HASH_ALGORITHM_NAME = "SHA-256";
/** 循环次数 **/
public final static int HASH_ITERATIONS = 15;
/** 执行加密-采用SHA256和盐值加密 **/
public static String sha256(String password, String salt) {
return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
}
}
ShiroUtils.java
package com.blog.tutorial07.shiro.utils;
import com.blog.tutorial07.shiro.entity.Users;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.LogoutAware;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisSessionDAO;
import java.util.Collection;
import java.util.Objects;
/**
* @Description Shiro工具类
* @Author youcong
*/public class ShiroUtils {
/**
* 私有构造器 **/ private ShiroUtils() {
}
private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
/**
* 获取当前用户Session * * @Author youcong
* @Return SysUserEntity 用户信息
*/ public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
/**
* 用户登出 * * @Author youcong
*/ public static void logout() {
SecurityUtils.getSubject().logout();
}
/**
* 获取当前用户信息 * * @Author youcong
* @Return SysUserEntity 用户信息
*/ public static Users getUserInfo() {
return (Users) SecurityUtils.getSubject().getPrincipal();
}
/**
* 删除用户缓存信息 * * @Author youcong
* @Param username 用户名称
* @Param isRemoveSession 是否删除Session
* @Return void
*/ public static void deleteCache(String username, boolean isRemoveSession) {
//从缓存中获取Session
Session session = null;
Collection sessions = redisSessionDAO.getActiveSessions();
Users sysUserEntity;
Object attribute = null;
for (Session sessionInfo : sessions) {
//遍历Session,找到该用户名称对应的Session
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
sysUserEntity = (Users) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (sysUserEntity == null) {
continue;
}
if (Objects.equals(sysUserEntity.getUserLogin(), username)) {
session = sessionInfo;
break;
}
}
if (session == null || attribute == null) {
return;
}
//删除session
if (isRemoveSession) {
redisSessionDAO.delete(session);
}
//删除Cache,在访问受限接口时会重新授权
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
}
SpringUtil.java
package com.blog.tutorial07.shiro.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @Description Spring上下文工具类
* @Author youcong
*/@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext context;
/**
* Spring在bean初始化后会判断是不是ApplicationContextAware的子类 * 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去 * @Author youcong
*/ @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 通过Name返回指定的Bean * @Author youcong
*/ public static T getBean(Class beanClass) {
return context.getBean(beanClass);
}
}
六、编写Controller
package com.blog.tutorial07.shiro.controller;
import com.blog.tutorial07.shiro.entity.Users;
import com.blog.tutorial07.shiro.service.UsersService;
import com.blog.tutorial07.shiro.utils.ShiroUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: youcong
* @time: 2020/11/14 13:27
*/@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UsersService usersService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录 * * @Author youcong
*/ @PostMapping("/login")
public Map login(@RequestParam String username, @RequestParam String password) {
Map map = new HashMap<>();
//进行身份验证
try {
//验证身份和登陆
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//进行登录操作
subject.login(token);
} catch (IncorrectCredentialsException e) {
map.put("code", 500);
map.put("msg", "用户不存在或者密码错误");
return map;
} catch (LockedAccountException e) {
map.put("code", 500);
map.put("msg", "登录失败,该用户已被冻结");
return map;
} catch (AuthenticationException e) {
map.put("code", 500);
map.put("msg", "该用户不存在");
return map;
} catch (Exception e) {
map.put("code", 500);
map.put("msg", "未知异常");
return map;
}
map.put("code", 0);
map.put("msg", "登录成功");
map.put("token", ShiroUtils.getSession().getId().toString());
return map;
}
/**
* 未登录 * * @Author youcong
*/ @RequestMapping("/unauth")
public Map unauth() {
Map map = new HashMap<>();
map.put("code", 500);
map.put("msg", "未登录");
return map;
}
@PostMapping("/list")
@RequiresRoles("1")
public String list() {
System.out.println("list:" + redisTemplate.opsForValue().get("list"));
if (StringUtils.isEmpty(redisTemplate.opsForValue().get("list"))) {
redisTemplate.opsForValue().set("list", usersService.list(), 360, TimeUnit.MINUTES);
}
return redisTemplate.opsForValue().get("list").toString();
}
}
七、测试(使用PostMan)
1.登录
查看redis,如图:
2.测试没有权限的接口
3.赋予权限再次测试
八、总结
无论是SpringSecurity还是Shiro,基本上整合非常相似,也很简单。
如果有朋友看完这篇文章还是不明白的话,可以访问如下地址:
https://github.com/developers...
将项目克隆到本地运行。这个git仓库,sql脚本什么的都有。
我本次用到的类基本上是基于这个的,只不过数据表不一样,我本次所使用的是wordpress的数据库。