随着企业级应用程序的复杂性不断增加,安全性成为开发过程中至关重要的一环。Spring Boot作为一种流行的Java开发框架,为开发人员提供了快速构建应用程序的便利性,而Shiro安全框架则为应用程序的安全性提供了可靠的解决方案。本技术文档将详细介绍如何将这两个强大的框架整合在一起,以构建安全可靠的企业级应用程序。
在本文中,我们将深入探讨如何使用Spring Boot和Shiro框架来实现认证、授权和安全策略的配置。我们还将分享一些优化技巧和最佳实践,以确保整合后的应用程序能够在安全性和性能方面达到最佳状态。此外,我们还将提供完整的项目代码,以便读者可以直接参考并应用于实际项目中。
无论您是初学者还是有经验的开发人员,本文都将为您提供全面的指导,帮助您轻松地将Spring Boot和Shiro安全框架整合到您的企业级应用程序中。
让我们一起深入探讨如何实现这一整合,并构建安全可靠的企业级应用程序!
项目地址:Spring Boot 整合 Shiro 安全框架
目录结构:
用户请求:用户向Web应用程序发出请求,例如访问某个页面或执行某个操作。
过滤器链:Shiro通过一系列的过滤器来处理用户请求。这些过滤器可以用于认证、授权、会话管理等操作。过滤器链的配置可以在Shiro的配置文件中进行定义。
认证:如果用户请求需要进行认证,Shiro将会检查用户是否已经登录。如果用户未登录,Shiro将会跳转到登录页面或者返回未认证的错误信息。
登录:用户在登录页面输入用户名和密码,提交登录请求。
认证处理:Shiro将会对用户提交的登录信息进行认证处理,例如验证用户名和密码是否匹配。
认证成功:如果认证成功,Shiro将会创建一个表示用户身份的Principal,并将其存储在会话中。
授权:在用户认证成功后,Shiro将会检查用户是否有权限执行所请求的操作。如果用户没有足够的权限,Shiro将会返回未授权的错误信息或者跳转到未授权页面。
执行操作:如果用户通过了认证和授权,Shiro将会允许用户执行所请求的操作,例如访问页面或者执行特定的功能。
会话管理:Shiro还提供了会话管理功能,用于跟踪用户的会话状态,例如会话超时、会话失效等。
响应:Web应用程序将会根据用户的请求和Shiro的处理结果生成相应的响应,返回给用户。
在数据库设计中,经典的五张表通常包括用户表、角色表、权限表、用户角色关联表和角色权限关联表。这些表通常用于实现用户身份认证和权限控制的功能。
用户表:用于存储系统中的用户信息,包括用户ID、用户名、密码、邮箱、电话号码等。用户表是系统中的核心表之一,用于存储用户的基本信息。
角色表:用于存储系统中的角色信息,包括角色ID、角色名称、角色描述等。角色表用于定义系统中的角色,例如管理员、普通用户、编辑等。
权限表:用于存储系统中的权限信息,包括权限ID、权限名称、权限描述、权限类型等。权限表用于定义系统中的各种权限,例如访问某个页面、执行某个操作等。
用户角色关联表:用于建立用户和角色之间的关联关系。这张表通常包括用户ID和角色ID两个字段,用于表示某个用户具有哪些角色。
角色权限关联表:用于建立角色和权限之间的关联关系。这张表通常包括角色ID和权限ID两个字段,用于表示某个角色具有哪些权限。
通过合理设计和使用这五张表,可以实现用户身份认证和权限控制的功能,为系统提供安全可靠的访问控制机制。同时,这些表也为系统的扩展和维护提供了良好的基础。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` bigint NOT NULL AUTO_INCREMENT,
`perm_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of permission
-- ----------------------------
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '超级管理员');
INSERT INTO `role` VALUES (2, '运营管理员');
INSERT INTO `role` VALUES (3, '商品管理员');
-- ----------------------------
-- Table structure for role_perm
-- ----------------------------
DROP TABLE IF EXISTS `role_perm`;
CREATE TABLE `role_perm` (
`rid` bigint NULL DEFAULT NULL,
`pid` bigint NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_perm
-- ----------------------------
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`salt` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '3e48d56a6fc20c50f589f562cdf20451', 'abcdefg123456');
INSERT INTO `user` VALUES (2, 'test', '2c4529400f6d307733040330b0f8df7d', 'abcdefg123456');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`uid` bigint NULL DEFAULT NULL,
`rid` bigint NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);
INSERT INTO `user_role` VALUES (1, 2);
SET FOREIGN_KEY_CHECKS = 1;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
doGetAuthenticationInfo方法:
用户登录认证方法,AuthenticationToken
参数就是登录接口中Subject
发起login
方法传的UsernamePasswordToken
类。根据获取到的用户名查询用户信息并填充SimpleAuthenticationInfo
返回对象。SimpleAuthenticationInfo
第一个参数是用户信息、第二个参数是用户密码、第三个参数是 Realm 名称,setCredentialsSalt
设置该用户的盐,因为用户密码采用了MD5加密加盐算法。
doGetAuthorizationInfo方法:
登录后授权认证方法,principalCollection
参数就是doGetAuthenticationInfo
方法中SimpleAuthenticationInfo
返回对象中第一个传入的user
用户信息。根据用户信息查询用户拥有的角色和权限信息并填充SimpleAuthorizationInfo
返回对象。
HashedCredentialsMatcher类:
加密处理。创建 HashedCredentialsMatcher
,指定算法名称和 HashIterations
(加密次数),将 HashedCredentialsMatcher
注入到 realm
中。
@Component
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// 加密处理
{
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1024);
this.setCredentialsMatcher(matcher);
}
/**
* 用户认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 判断用户名(非空)
if(StringUtils.isEmpty(username)){
// 返回null,默认抛出一个异常:org.apache.shiro.authc.UnknownAccountException
return null;
}
// 根据用户名查询用户
User user = userService.findByUsername(username);
// 判断用户是否为null
if(user == null) {
return null;
}
// 声明 SimpleAuthenticationInfo 对象,并填充用户信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), "CustomRealm");
// 设置盐
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
return simpleAuthenticationInfo;
}
/***
* 授权认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 判断是否认证
Subject subject = SecurityUtils.getSubject();
if (subject == null || !subject.isAuthenticated()) {
return null;
}
// 获取认证用户
User user = (User) principalCollection.getPrimaryPrincipal();
// 根据用户获取当前用户拥有的角色
Set<Role> roleSet = roleService.findRolesByUid(user.getId());
Set<Long> roleIdSet = new HashSet<>();
Set<String> roleNameSet = new HashSet<>();
for (Role role : roleSet) {
roleIdSet.add(role.getId());
roleNameSet.add(role.getRoleName());
}
// 根据用户拥有的角色查询权限信息
Set<Permission> permSet = permissionService.findPermsByRoleSet(roleIdSet);
Set<String> permNameSet = new HashSet<>();
for (Permission permission : permSet) {
permNameSet.add(permission.getPermName());
}
// 声明 SimpleAuthorizationInfo 对象,并填充角色信息和权限信息
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
simpleAuthenticationInfo.setRoles(roleNameSet);
simpleAuthenticationInfo.setStringPermissions(permNameSet);
return simpleAuthenticationInfo;
}
}
以下是Shiro中常用的过滤器:
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
filterChainDefinitionMap.put("/login.html","anon");
filterChainDefinitionMap.put("/user/**","anon");
filterChainDefinitionMap.put("/test/rememberMe","user");
filterChainDefinitionMap.put("/test/authentication","authc");
filterChainDefinitionMap.put("/test/select","roles[超级管理员,运营管理员]");
filterChainDefinitionMap.put("/**","authc");
shiroFilterChainDefinition.addPathDefinitions(filterChainDefinitionMap);
return shiroFilterChainDefinition;
}
application.yml
shiro:
loginUrl: /login.html
# 针对过滤器链生效,针对注解是不生效的
unauthorizedUrl: /401.html
如果用户未认证会默认跳到login.html页面,如果用户认证了但是权限不够会默认跳到/401.html页面。这是都是可配置的。
以下是自定义一个满足角色集合其中一个角色就放行的过滤器。
public class RolesOrAuthorizationFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
// 获取主体subject
Subject subject = getSubject(request, response);
// 将传入的角色转成数组操作
String[] rolesArray = (String[]) mappedValue;
// 健壮性校验
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
// 开始校验
for (String role : rolesArray) {
if(subject.hasRole(role)){
return true;
}
}
return false;
}
}
注入到 ShiroFilterFactoryBean
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) {
// 构建ShiroFilterFactoryBean工厂
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 设置了大量的路径
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
// 设置安全管理器
filterFactoryBean.setSecurityManager(securityManager);
// 设置过滤器链
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
// 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
filterFactoryBean.getFilters().put("rolesOr", new RolesOrAuthorizationFilter());
// 返回工厂
return filterFactoryBean;
}
在Shiro中,权限注解用于在代码中标记需要进行权限控制的方法或类。Shiro提供了多种权限注解,常用的包括:
@RequiresPermissions:用于标记需要进行权限验证的方法,指定需要的权限字符串。例如:@RequiresPermissions(“user:create”) 表示当前方法需要用户具有"user:create"权限才能访问。
@RequiresRoles:用于标记需要进行角色验证的方法,指定需要的角色名称。例如:@RequiresRoles(“admin”) 表示当前方法需要用户具有"admin"角色才能访问。
@RequiresGuest:用于标记当前方法只允许未认证的用户访问,即游客访问。
@RequiresUser:用于标记当前方法只允许已认证的用户访问,即已登录用户访问。
@RequiresAuthentication:用于标记当前方法需要进行身份验证才能访问,即需要用户进行登录认证。
这些注解可以在方法级别或类级别进行标记,以实现对应的权限控制。通过在代码中使用这些注解,可以方便地实现基于Shiro的权限控制功能。
@GetMapping("/update")
@RequiresRoles(value = {"超级管理员","运营管理员"})
public String update(){
return "item Update!!!";
}
@GetMapping("/insert")
@RequiresRoles(value = {"超级管理员","运营管理员"}, logical = Logical.OR)
public String insert(){
return "item Update!!!";
}
注意: 注解的形式无法将错误页面的信息定位到401.html
,因为配置的这种路径,只针对过滤器链有效,注解无效。为了实现友好提示的效果,可以配置异常处理器,@RestControllerAdvice
,@ControllerAdvice
在服务搭建集群后,或者是服务是分布式架构的,导致单台服务的认证无法让其他服务也得知到信息:
@Component
public class RedisSessionDAO extends AbstractSessionDAO {
private final String SHIOR_SESSION = "session:";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId, session, 30, TimeUnit.MINUTES);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
if (session != null) {
redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
if (session == null) {
return;
}
redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(), session, 30, TimeUnit.MINUTES);
}
@Override
public void delete(Session session) {
if (session == null) {
return;
}
redisTemplate.delete(SHIOR_SESSION + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<String> keys = redisTemplate.keys(SHIOR_SESSION + "*");
Set<Session> sessionSet = new HashSet<>();
// 使用Redis管道
List<Object> results = redisTemplate.executePipelined((RedisConnection connection) -> {
for (String key : keys) {
connection.get(key.toString().getBytes());
}
return null;
});
// 处理管道返回的结果
for (Object result : results) {
if (result != null) {
Session session = (Session) result;
sessionSet.add(session);
}
}
return sessionSet;
}
}
将RedisSessionDAO
交给SessionManager
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
将SessionManager
注入到SecurityManager
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
将传统的ConcurrentHashMap
切换为Redis
之后,发现每次请求需要访问多次Redis
服务,这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis
的压力也提高了。
解决方案:基于装饰者模式重新声明SessionManager
中提供的retrieveSession
方法,让每次请求先去request
域中查询session
信息,request
域中没有,再去Redis
中查询。
public class DefaultRedisWebSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
if(sessionKey instanceof WebSessionKey){
WebSessionKey webSessionKey = (WebSessionKey) sessionKey;
ServletRequest request = webSessionKey.getServletRequest();
Session session = (Session) request.getAttribute(sessionId + "");
if(session != null){
return session;
}else{
session = retrieveSessionFromDataSource(sessionId);
if (session == null) {
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
request.setAttribute(sessionId + "", session);
return session;
}
}
return null;
}
}
配置DefaultRedisWebSessionManager
到SecurityManager
中
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
如果后台接口存在授权操作,那么每次请求都需要去数据库查询对应的角色信息和权限信息,对数据库来说,这样的查询压力太大了。
在Shiro中,发现每次在执行自定义Realm的授权方法查询数据库之前,会有一个执行Cache的操作。
先从Cache中基于一个固定的key去查询角色以及权限的信息。
只需要提供好响应的CacheManager实例,还要实现一个与Redis交互的Cache对象,将Cache对象设置到CacheManager实例中。
实现 RedisCache
@Component
public class RedisCache<K, V> implements Cache<K, V> {
@Resource
private RedisTemplate redisTemplate;
private final String CACHE_PREFIX = "cache:";
@Override
public V get(K k) throws CacheException {
V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
if(v != null){
redisTemplate.expire(CACHE_PREFIX + k,15, TimeUnit.MINUTES);
}
return v;
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(CACHE_PREFIX + k, v,15, TimeUnit.MINUTES);
return v;
}
@Override
public V remove(K k) throws CacheException {
V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
if(v != null){
redisTemplate.delete(CACHE_PREFIX + k);
}
return v;
}
public void clear() throws CacheException {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
redisTemplate.delete(keys);
}
@Override
public int size() {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
return keys.size();
}
@Override
public Set<K> keys() {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
return keys;
}
@Override
public Collection<V> values() {
Set values = new HashSet();
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
for (Object key : keys) {
Object value = redisTemplate.opsForValue().get(key);
values.add(value);
}
return values;
}
}
实现 CachaManager
@Component
public class RedisCacheManager implements CacheManager {
@Resource
private RedisCache redisCache;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}
}
将RedisCacheManager
配置到SecurityManager
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
查询记录
查询Redis
查询Mysql数据库
查询Redis
查询Redis
查询Redis
查询Redis
查询Redis
查询Redis
查询Redis