Shiro是Apache下的一个开源的,强大且易用的Java安全框架,可以执行身份验证、授权、密码和会话管理。相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。因为作者使用的前后端分离开发模式,引入SpringSecurity还会给前端开发人员一定的工作量和兼容性问题。结合实际情况,最终采用Shiro作为权限控制安全框架。
1.shiro官方架构图
2. 主要功能
2.1 三个核心
(1) Subject
即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数的目的和用途,你可以把它认为是Shiro的“用户”概念。Subject代表了当前用户的安全操作,而SecurityManager则管理所有用户的安全操作。
(2) SecurityManager
它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
(3) Realm
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
2.2 相关功能类
1. Authentication:身份认证/登录(账号密码验证)。
2. Authorization:授权,即角色或者权限验证。
3. Session Manager:会话管理,用户登录后的session相关管理。
4. Cryptography:加密,例如密码加密等。
5. Web Support:Web支持,集成Web环境。
6. Caching:缓存。把用户信息、角色、权限等信息缓存到如redis等缓存中。
7. Concurrency:多线程并发验证。在一个线程中开启另一个线程,可以把权限自动传播过去。
8.Web Integration web:系统集成。
9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
10. Remember Me:记住我,登录后,下次再来的话不用登录了。
11.Interations:集成其它应用,spring、缓存框架。
3. Spring Boot整合Shiro
3.1 pom.xml中添加依赖
<!-- shiro相关依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
3.2 Shiro的配置类
@Configuration
public class ShiroConfig {
/**
* 配置Shiro核心 安全管理器 SecurityManager
* SecurityManager安全管理器:所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;负责与后边介绍的其他组件进行交互。(类似于SpringMVC中的DispatcherServlet控制器)
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//将自定义的realm交给SecurityManager管理
securityManager.setRealm(userRealm);
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(SessionManager());
// 使用记住我
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/*
自定义Realm
*/
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
/**
* 配置Shiro的Web过滤器,拦截浏览器请求并交给SecurityManager处理
*
* @return
*/
@Bean
public ShiroFilterFactoryBean webFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置拦截链 使用LinkedHashMap,因为LinkedHashMap是有序的,shiro会根据添加的顺序进行拦截
// Map<K,V> K指的是拦截的url V值的是该url是否拦截
Map<String, String> filterChainMap = new LinkedHashMap<String, String>(16);
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问,先配置anon再配置authc。这里可不配置。
//filterChainMap.put("/debug/test1", "anon");
// filterChainMap.put("/debug/test", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
/**
* 开启aop注解支持
* 即在controller中使用 @RequiresPermissions("")
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
//设置安全管理器
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* redisManager
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("127.0.0.1");
redisManager.setPort(6379);
// 配置过期时间 一周
redisManager.setExpire(604800);
return redisManager;
}
/**
* cacheManager
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* redisSessionDAO
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* sessionManager
*/
public DefaultWebSessionManager SessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(604800000L);
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* cookie对象;
* @return
*/
public SimpleCookie rememberMeCookie(){
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//cookie生效时间30天,单位秒;
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
/**
* cookie管理对象;记住我功能
* @return
*/
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// cookieRememberMeManager.setCipherKey用来设置加密的Key,参数类型byte[], 字节数组长度要求16;
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
}
3.3 Shiro的自定义Realm
**
* 自定义Realm
* (1)AuthenticatingRealm:shiro中的用于进行认证的领域,实现doGetAuthentcationInfo方法实现用户登录时的认证逻辑;
* (2)AuthorizingRealm:shiro中用于授权的领域,实现doGetAuthrozitionInfo方法实现用户的授权逻辑,AuthorizingRealm继承了AuthenticatingRealm,
* 所以在实际使用中主要用到的就是这个AuthenticatingRealm类;
* (3)AuthenticatingRealm、AuthorizingRealm这两个类都是shiro中提供了一些线程的realm接口
* (4)在与spring整合项目中,shiro的SecurityManager会自动调用这两个方法,从而实现认证和授权,可以结合shiro的CacheManager将认证和授权信息保存在缓存中,
* 这样可以提高系统的处理效率。
*
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private RedissonClient redissonClient;
@Autowired
private UserService userService;
@Override
/**
* 认证
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//编写shiro判断逻辑,判断用户名和密码
//1.判断用户名 token中的用户信息是登录时候传进来的
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
//在自己数据库找当前用户
User user = userSerivce.findByName(usernamePasswordToken.getUsername());
if(user == null){
//用户名不存在
return null;//shiro底层会抛出UnKnowAccountException
}
//2.判断密码
//第二个字段是user.getPassword(),注意这里是指从数据库中获取的password。第三个字段是realm,即当前realm的名称。
//这块对比逻辑是先对比username,但是username肯定是相等的,所以真正对比的是password。
//从这里传入的password(这里是从数据库获取的)和token(filter中登录时生成的)中的password做对比,如果相同就允许登录,
// 不相同就抛出IncorrectCredentialsException异常。
//如果认证不通过,就不会执行下面的授权方法了
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword, getName());
//3.返回身份处理对象
return simpleAuthenticationInfo;
}
@Override
/**
* 授权
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
//1.获取当前登录的用户
User user = (User) principal.getPrimaryPrincipal();
//通过SimpleAuthenticationInfo做授权
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//2.添加角色
//这里从缓存获取当前用户的角色信息,并赋予simpleAuthorizationInfo
RMapCache<String, Set<String>> userRoleCodeCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
Set<String> roleCode = roleCache.get(user.getId());
if (!CollectionUtils.isEmpty(roleCode)) {
simpleAuthorizationInfo.addRoles(roleCode);
}
//3.添加权限
//这里从缓存获取当前用户的权限信息,并赋予simpleAuthorizationInfo
RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_PERMISSION);
Set<String> userPermissionCode = userPermissionCodeCache.get(user.getId());
if (!CollectionUtils.isEmpty(userFunPermissionCode)) {
simpleAuthorizationInfo.addStringPermissions(userFunPermissionCode);
}
return simpleAuthorizationInfo;
}
}
3.4 自定义Shiro异常拦截
当Shiro抛出UnauthorizedException,表明当前用户没有权限。
当Shiro抛出UnauthenticatedException,表明当前用户没有被Shiro管理,也就是没有登录,让用户重新登录。
我们在这统一拦截异常并封装返回异常结果。
@ControllerAdvice
public class ExceptionController extends BaseApp {
@ResponseBody
@ExceptionHandler(value = {UnauthorizedException.class,UnauthenticatedException.class})
public Map<String, Object> handleClientException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
if (e instanceof UnauthorizedException) {
resp.setStatus(HttpStatus.FORBIDDEN.value());
return buildResponse(EBusinessCode.NOT_PERMISSION, null);
} else if (e instanceof UnauthenticatedException) {
resp.setStatus(HttpStatus.FORBIDDEN.value());
return buildResponse(EBusinessCode.RE_LOGIN, null);
}
}
}
3.5 测试
//将当前用户交给Shiro管理,这里的处理逻辑可加在原有项目的登录逻辑里面。
@RequestMapping(value = "/test1", method = RequestMethod.GET)
public @ResponseBody Map<String, Object> Test1() throws BaseException {
User user = new User();
user.setId("123456");
user.setName("123456");
user.setPassword("123456");
//1.将user交给shiro。
UsernamePasswordToken token = new UsernamePasswordToken(user.getName, user.getPassword());
token.setRememberMe(true);
Subject currentUser = SecurityUtils.getSubject();
//主体提交登录请求到SecurityManager
currentUser.login(t);
//2.查找用户角色和权限,并将用户的角色和权限放到缓存。
//添加角色
RMapCache<String, Set<String>> roleCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
Set<String> roleCode = new HashSet<>();
roleCode.add("superMan");
roleCache.put(user.getId(),roleCode)
//添加权限
RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_FUNCTION_PERMISSION);
Set<String> permissionCode = new HashSet<>();
permissionCode.add("1000");
userPermissionCodeCache.put(user.getId(),permissionCode)
return buildResponse();
}
//对这个接口实行权限控制,前提需要把当前用户交给Shiro管理,如果Shiro识别不到当前用户,则会抛出UnauthenticatedException异常,让用户重新登录。
//这里表明需要当前用户同时拥有角色为superMan和root,且拥有代号为1000或1001的权限才能访问,否则抛出UnauthorizedException,表明当前用户没有权限访问该接口。
//logical = Logical.OR表示或的关系,logical = Logical.AND表示且的关系
@RequestMapping(value = "/test", method = RequestMethod.GET)
@RequiresRoles(value = {"superMan","root"}, logical = Logical.AND)
@RequiresPermissions(value = {"1000", "1001"}, logical = Logical.OR) public @ResponseBody
Map<String, Object> Test() throws Exception {
return buildResponse();
}