概述
Shiro是一个功能强大且灵活的开源Java安全框架,相比于SpringSecurity更加简单,Shiro可以执行身份验证、授权、加密和会话管理等
Shiro的主要功能如下图:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- SessionManager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明储;
- Web Support:Web支持,可以非常容易的集成到Web 环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- RememberMe:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
从应用程序的角度来看Shiro是如何完成工作的,如下图:
主要涉及3个概念 - Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色
- Realm:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource
详细架构如下图:
- Subject(org.apache.shiro.subject.Subject):可以看到主体可以是任何可以与应用交互的 “用户”; - SecurityManager(org.apache.shiro.mgt.SecurityManager):相当于SpringMVC中的DispatcherServlet;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证、授权、会话及缓存的管理。
- Authenticator(org.apache.shiro.authc.Authenticator):负责Subject认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authorizer (org.apache.shiro.authz.Authorizer):授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- SessionManager(org.apache.shiro.session.mgt.SessionManager):管理Session生命周期的组件;而Shiro并不仅仅可以用在Web 环境,也可以用在如普通的JavaSE环境
- CacheManager (org.apache.shiro.cache.CacheManager):缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
- Cryptography(org.apache.shiro.crypto.*):密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密。
- Realms(org.apache.shiro.realm.Realm):可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的Realm;
更多关于Shiro的知识,请阅读Shiro官网
SpringBoot整合Shiro
技术栈
本文主要使用的技术如下:
JDK1.8
Maven3.6.1
SpringBoot2.3.4
shiro-spring-boot-web-starter1.6.0(springboot提供)
spring-boot-starter-thymeleaf
bootstrap
引入依赖
核心依赖如下:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
配置文件
@Configuration
public class ShiroConfig {
/**
* 配置自定义Realm
* @return
*/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
/**
* 配置安全管理器
* @return
*/
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
/**
* 配置过滤条件和跳转条件
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//认证失败,跳转到登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
//认证成功,跳转到首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//权限认证失败,跳转到无权限页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
//配置过滤条件
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//过滤静态页面
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
//过滤swagger API接口页面
filterChainDefinitionMap.put("/swagger-ui", "anon");
filterChainDefinitionMap.put("/swagger-ui/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v3/api-docs", "anon");
filterChainDefinitionMap.put("/csrf", "anon");
//登录和退出不需要拦截
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/unauth", "anon");
//其他都需要拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 配置Shiro方言,用于shiro整合thymeleaf
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* 开启Shiro注解通知器
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SessionsSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置密码适配器
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密迭代次数
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 配置动态代理
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
advisorAutoProxyCreator.setUsePrefix(true);
return advisorAutoProxyCreator;
}
}
核心代码
自定义Realm实现AuthorizingRealm接口的两个方法,主要代码如下:
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("Shiro授权....");
SysUser user = (SysUser) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//查询角色信息
Set<String> roles = new HashSet<String>();
//查询菜单权限信息
Set<String> perms = new HashSet<String>();
roles = roleService.selectRoleCodesByUserId(user.getId());
perms = menuService.selectPermsByUserId(user.getId());
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(perms);
return authorizationInfo;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("Shiro认证...");
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
String username = userToken.getUsername();
SysUser user = null;
try {
user = userService.login(username);
} catch (UserNotExistsException e) {
throw new UnknownAccountException(e.getMessage(), e);
} catch (UserBlockedException e) {
throw new LockedAccountException(e.getMessage(), e);
} catch (Exception e) {
logger.warn("验证用户[" + username + "]未通过!");
throw new AuthenticationException(e.getMessage(), e);
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
return authenticationInfo;
}
登录接口如下:
@Controller
public class LoginController {
private static Logger logger = LoggerFactory.getLogger(LoginController.class);
@GetMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response) {
return "login";
}
@RequestMapping("/login")
@ResponseBody
public AjaxResult login(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
logger.info("登录成功!");
return AjaxResult.ok().message("登录成功!");
} catch (UnknownAccountException e) {
return AjaxResult.error().message("用户名或密码不正确!");
} catch (IncorrectCredentialsException e) {
return AjaxResult.error().message("用户名或密码不正确!");
}
}
@GetMapping("/unauth")
public String unauth() {
return "unauth";
}
}
前端使用bootstrap和jquery实现,登录界面代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>登录</title>
<link href="../static/favicon.ico" th:href="@{favicon.ico}" rel="icon"/>
<link href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet" type="text/css"/>
<link href="../static/css/login.min.css" th:href="@{/css/login.min.css}" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="container">
<form class="form-signin">
<h2 class="form-signin-heading">登录</h2>
<label for="username" class="sr-only">用户名:</label>
<input type="text" id="username" class="form-control" placeholder="用户名" autofocus>
<label for="password" class="sr-only">密码</label>
<input type="password" id="password" class="form-control" placeholder="密码" required>
<!--<div class="checkbox">
<label>
<input type="checkbox" value="rememberMe">记住我
</label>
</div>-->
<button class="btn btn-lg btn-primary btn-block" type="button" onclick="login()">登录</button>
</form>
</div>
<script src="../static/js/jquery-3.5.1.min.js" th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script>
<script src="../static/js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
<script>
function login() {
var username = $("#username").val();
var password = $('#password').val();
$.ajax({
type: "post",
url: "/login",
data: {
username: username,
password: password
},
success: function(data) {
if (data.code == 20000) {
location.href = '/index';
} else {
alert(data.message);
}
}
});
return false;
}
</script>
</body>
</html>
效果展示
启动项目后,浏览器输入http://localhost:8089
输入用户名和密码:admin/123456进入首页
点击有权限的用户查询会返回查询的数据,没有授权的会提示无权限访问
Shiro自带的常见异常有:
DisabledAccountException:账户被禁用
LockedAccountException:账户被锁定
UnknownAccountException:账户不存在
ExcessiveAttemptsException:登录失败次数过多
IncorrectCredentialsException:密码不正确
ExpiredCredentialsException:密码已过期
完整代码详见码云
若想了解SpringBoot整合SpringSecurity+jwt的步骤,请参阅SpringBoot整合SpringSecurity+Jwt实现权限管理 若想了解SpringBoot整合SpringSecurity的步骤,请参阅SpringBoot整合SpringSecurity实现权限管理