公司新项目用的是shiro做权限控制,一直说写一篇shiro的文章,一直拖着没写。马上过年了, 这债该还了呀。。。
项目基于springboot(2.1.7.RELEASE) + mybatis-plus(3.2.0) + shiro-redis(3.2.3)
原理参考:1、shiro框架详解。2、Shiro权限管理框架详解。
有些名词还是得先了解:
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager : 安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等;
FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator(authc):认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer(authz):Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached,redis 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;也可以使用开源shiro-redis。
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密
继承AuthorizingRealm抽象类,重写doGetAuthenticationInfo方法验证用户账号密码,以及doGetAuthorizationInfo授予用户权限。注意:doGetAuthenticationInfo方法不验证密码,这交给shiro内部去做,我们在后面设置密码验证方式就好。
public class UserRealm extends AuthorizingRealm {
@Autowired
private UUserMapper userMapper;
/**
* 赋予角色,权限
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户名
UUser user = (UUser) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//设置用户角色
Set rolesEntity = userMapper.getRolesByUsername(user.getEmail());
//设置角色
authorizationInfo.setRoles(rolesEntity.stream().map(URole::getName).collect(Collectors.toSet()));
//设置权限
Set roleIds = rolesEntity.stream().map(URole::getId).collect(Collectors.toSet());
authorizationInfo.setStringPermissions(userMapper.getPermissionsByRoles(roleIds));
return authorizationInfo;
}
/**
* 账号密码
* 获取账号凭证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken instanceof UsernamePasswordToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UUser::getEmail, token.getUsername());
UUser user = userMapper.selectOne(wrapper);
//账号是否存在
if (Objects.isNull(user)) {
throw new AuthenticationException("账号或密码错误");
} else {
//账号是否被锁定
if (!new Long(1).equals(user.getStatus())) {
throw new LockedAccountException("账号已被锁定");
}
//返回账号密码验证凭证信息
//这里不验证密码,交给shiro内部去做
return new SimpleAuthenticationInfo(user,
user.getPswd(),
ByteSource.Util.bytes(user.getSalt()),
getName());
}
} else {
log.error("账号验证错误:{}", authenticationToken);
throw new UnknownAccountException("账号验证错误");
}
}
}
配置上面一些shiro名词。提一下HashedCredentialsMatcher这个bean,这里就是设置密码校验方式为MD5(doGetAuthenticationInfo方法没有验证密码)。shiro还可以限制同一账号同时在线人数......
@Configuration
public class ShiroConfig implements EnvironmentAware {
private Environment environment;
//----------------------------配置缓存相关开源包shiro-redis------------------------------------------------
//配置redisSessionDAO
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
//配置cacheManager
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//配置redisManager
public IRedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(environment.getProperty("shiro-redis.port"));
String password = environment.getProperty("shiro-redis.password");
if (StringUtils.isNotBlank(password)) {
redisManager.setPassword(password);
}
// redisManager.setTimeout((int) EXPIRE_SECONDS);
return redisManager;
}
//---------------------------------结束---------------------------------------------------------------------
/**
* 设置会话管理器
*
* @return
*/
@Bean("sessionManager")
public SessionManager mySessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setCacheManager(cacheManager());
return sessionManager;
}
/**
* 设置安全管理器
*
* @param sessionManager
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 过滤器
* 哪些接口/页面授权登录就能访问
*
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/u-user/login");
shiroFilter.setUnauthorizedUrl("/");
//anon 放行;authc,需要权限验证
Map filterMap = new LinkedHashMap<>();
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/statics/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/u-user/login", "anon");
filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1);//散列的次数,比如散列两次,相当于 md5(md5(""));
// hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* 安全实体数据源
* 获取用户实体
*
* @return
*/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
userRealm.setCachingEnabled(true);
//启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
userRealm.setAuthenticationCachingEnabled(false);
//缓存AuthenticationInfo信息的缓存名称
userRealm.setAuthenticationCacheName("authenticationCache");
//启用授权缓存,即缓存AuthorizationInfo信息,默认false
userRealm.setAuthorizationCachingEnabled(true);
//缓存AuthorizationInfo信息的缓存名称
userRealm.setAuthorizationCacheName("authorizationCache");
//设置缓存管理器
userRealm.setCacheManager(cacheManager());
return userRealm;
}
//==================================以下为配置注解相关==============================================================
/**
* Shiro生命周期处理器
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
登录:使用Subject subject = SecurityUtils.getSubject(); subject.login(token);登录。
权限控制:使用注解@RequiresPermissions,@RequiresRoles,@RequiresUser,@RequiresGuest,@RequiresAuthentication来控制访问权限。至于数据权限控制,只能在sql代码里加限制了......。
@RestController
@RequestMapping("/u-user")
public class UUserController {
@Autowired
private IUUserService userService;
/**
* 用户登录
*
* @param params 请求参数
* @return
*/
@PostMapping("/login")
public R login(@RequestBody LoginReqDto params) {
UsernamePasswordToken token = new UsernamePasswordToken(params.getEmail(), params.getPassword());
try {
// 获取 subject 认证主体
Subject subject = SecurityUtils.getSubject();
subject.login(token);
return R.ok().put("data", userService.getById(1));
} catch (LockedAccountException e) {
return R.error("账号被锁定");
} catch (IncorrectCredentialsException e) {
return R.error("账号或密码错误");
} catch (AuthenticationException e) {
return R.error("账户验证失败");
}
}
/**
* 查找所有用户信息list
*
* @return 响应实体
*/
@GetMapping("/list")
@RequiresPermissions(value = {"user:list"}, logical = Logical.OR)
public R getAllUsers() {
return R.ok().put("data", userService.list());
}
}
完整代码在:https://gitee.com/Canon_Canon/shirodemo.git,包含了sql文件。启动后进入swagger-ui控制台http://localhost:8080/swagger-ui.html
完~