6.Shiro权限认证Web案例
6.1.Shiro内置的过滤器
- 核心过滤器类:DefaultFilter,配置那个路径对应那个拦截器进行处理
authc:org.apache.shiro.web.filter.authc.FromAuthenticationFilter - 需要认证登录才能访问
user:org.apache.shiro.web.filter.authc.UserFilter - 用户拦截器,表示必须存在用户
anon:org.apache.shiro.web.filter.authc.AnonymousFilter - 匿名拦截器,不需要登录即可访问的资源,匿名用户或者游客,一般用于过滤静态资源
roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter - 角色授权拦截器,验证用户是否拥有角色。 - 参数可以写多个,多个参数时写roles["admin","user"],当多个参数时必须每个参数都通过才算通过。
perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter - 权限授权拦截器,验证用户是否拥有权限 - 参数可写多个,和角色多个是一致的
authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter - httpBasic身份验证拦截器
logout:org.apache.shiro.web.filter.authc.LogoutFilter - 退出拦截器,执行后会执行跳转到shiroFilterFactoryBean.setLoginUrl()设置的url
port:org.apache.shiro.web.filter.authz.PortFilter - 端口拦截器,可通过的端口
ssl:org.apache.shiro.web.filter.authz.SslFilter - ssl拦截器,只有请求协议是https才能通过
6.2.Shiro的Filter配置路径
- 路径支持通配符,完整匹配,注意匹配符不包括分隔符"/"
- 路径通配符支持?、*、**,注意通配符匹配不包括目录分隔符"/"
- * 可以匹配所有。不加 * 可以进行前缀匹配,但多个冒号就需要多个 * 来匹配
URL权限采取第一次匹配优先的方式,优先匹配靠前的规则 ?:匹配一个字符,如/user? 匹配/user3,但不匹配/user/ *:匹配0个或多个字符串,如/add*,匹配/addtest,但不匹配/add/1 ** : 匹配路径中的零个或多个路径,如 /user/** 将匹 配 /user/xxx 或 /user/xxx/yyy 例子 /user/**=filter1 /user/add=filter2 请求 /user/add 命中的是filter1拦截器
- 性能问题:通配符比字符串匹配会复杂点,所以性能也会稍弱,推荐使用字符匹配方式
6.3.Shiro数据安全之数据加解密
(1)为啥要加解密
- 明文数据容易泄露,比如密码铭文存储,万一泄露则会造成严重的后果
(2)什么是散列算法
- 一般叫hash,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,适合存储密码,比如MD5
- (3)什么是salt(盐)
- 如果直接通过散列函数得到的加密数据,容易被对应解密网站暴力破解,一般会在应用层序里面加特殊的自动进行处理,比如用户id等等,唯一标识的东西。
4)Shiro里面CredentialsMatcher,用来验证密码是否正确
源码:AuthenticatingRealm -> assertCredentialsMatch()
(5)自定义验证规则
一般会自定义验证规则 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //散列算法,使用MD5算法; hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列的次数,比如散列两次,相当于 md5(md5("xxx")); hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; }
6.4.Shiro权限控制注解
@RequiresRoles(value={"admin","editor"},logical=Logical.AND) 需要角色admin和editor两个角色同时满足 @RequiresRoles(value={"admin","editor"},logical=Logical.OR) 需要角色admin或editor两个角色其中一个满足 @RequiresAuthentication 已经授过权,调用Subject.isAuthenticated()返回true @RequiresUser 身份验证或者通过记 住我登录的
查用API
subject.hasRole("xxx") subject.isPermitted("xxx") subject.isPermittedAll("xxx","xxx") subject.checkRole("xxx")
6.5.Shiro缓存模块讲解
- AuthenticatingRealm 及 AuthorizingRealm 分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓存.
6.6.Shiro Session模块讲解
(1)什么是session会话
用户和程序直接的链接,程序可以根据session识别到哪个用户,和javaweb中的session类似
(2)什么是会话管理器SessionManager
- 会话管理器管理所有subject的所有操作,是shiro的核心组件
- 核心方法
//开启一个session Session start(SessionContext context) //指定key获取session Session getSession(SessionKey key)
(3)SessionDao会话存储/持久化
- SessionDAO
- AbstractSessionDAO
- CachingSessionDAO
- EnterpeiseCacheSessionDAO
- MemorySessionDAO
- 核心方法
//创建 Serializable create(Session session) //获取 Session readSession(Serializable sessionId) throws UnknownSessionException //更新 void update(Session session) //删除,会话过期时调用 void delete(Session session) //获取活跃的session Collection<Session> getActiveSessions()
RememberMe 1.Cookie写到客户端并保存 2.通过调用subject.login()前,设置 token.setRememberMe(true) - subject.isAuthenticated() 表示用户进行了身份验证登录的,即Subject.login 进行了登录 - subject.isRemembered() 表示用户是通过RememberMe登录的 - subject.isAuthenticated()==true,则 subject.isRemembered()==false, 两个互斥 - 总结:特殊页面或者API调用才需要authc进行验证拦截,该拦截器会判断用户是否是通过
7.SpringBoot2.x整合Shiro
7.1.数据库设计
- user表
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(128) DEFAULT NULL COMMENT '用户名', `password` varchar(256) DEFAULT NULL COMMENT '密码', `create_time` datetime DEFAULT NULL, `salt` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- role表
CREATE TABLE `role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL COMMENT '名称', `description` varchar(64) DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- user_role表
CREATE TABLE `user_role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `user_id` int(11) DEFAULT NULL, `remarks` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
- permission表
CREATE TABLE `permission` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL COMMENT '名称', `url` varchar(128) DEFAULT NULL COMMENT '接口路径', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
- role_permission表
CREATE TABLE `role_permission` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `permission_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
7.2.Maven项目搭建
创建SpringBoot项目,引入依赖,配置数据库
<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>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--阿里巴巴druid数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> <!--spring整合shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </pluginRepository> </pluginRepositories>
#==============================数据库相关配置======================================== spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.10.88:3306/rbac_shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username =root spring.datasource.password =123456 #使用阿里巴巴druid数据源,默认使用自带的 #spring.datasource.type =com.alibaba.druid.pool.DruidDataSource #开启控制台打印sql mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # mybatis 下划线转驼峰配置,两者都可以 #mybatis.configuration.mapUnderscoreToCamelCase=true mybatis.configuration.map-underscore-to-camel-case=true
7.3.编写查询用户全部信息接口
(1)实体类编写
- User
/** * 用户表 */ public class User { private int id; private String username; private Date createTime; private String salt; private List<Role> roleList; }
- Role
/** * 角色表 */ public class Role { private int id; private String name; private String description; private List<Permission> permissionList; }
- UserRole
/** * 用户角色中间表 */ public class UserRole { private int id; private int userId; private int roleId; }
- Permission
/** * 权限表 */ public class Permission { private int id; private String name; private String url; }
- RolePermission
/** * 权限角色中间表 */ public class RolePermission { private int id; private int roleId; private int permissionId; }
(2)Mapper编写
- UserMapper
public interface UserMapper { @Select("select * from user where username = #{username}") User findByUsername(@Param("username") String username); @Select("select * from user where id = #{id}") User findById(@Param("id") int id); @Select("select * from user where username = #{username} and password = #{pwd}") User findByUsernameAndPwd(@Param("username") String username,@Param("pwd") String pwd); }
- RoleMapper
public interface RoleMapper { @Select("select * from user_role where user_id = #{userId}") List<UserRole> findRolesByUserId(@Param("userId") int userId); @Select("select * from role where id = #{roleId}") List<Role> findRolesByRoleId(@Param("roleId") int roleId); }
- PermissionMapper
public interface PermissionMapper { @Select("select * from permission where id = #{roleId}") List<Permission> findPermissionsByRoleId(@Param("roleId") int roleId); }
(3)UserService编写
- UserService
public interface UserService { /** * 获取用户全部信息,包括角色权限 * @param username * @return */ User findAllUserInfoByUsername(String username); /** * 获取用户基本信息 * @param userId * @return */ User findSimpleUserInfoById(int userId); /** * 获取用户基本信息 * @param username * @return */ User findSimpleUserInfoByUsername(String username); }
- UserServiceImpl
@Service public class UserServiceImpl implements UserService { @Autowired private RoleMapper roleMapper; @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Override public User findAllUserInfoByUsername(String username) { User user = userMapper.findByUsername(username); List<UserRole> userRoles = roleMapper.findRolesByUserId(user.getId()); List<Role> roles = new ArrayList<>(); for (UserRole role : userRoles) { roles = roleMapper.findRolesByRoleId(role.getRoleId()); for (Role x : roles) { x.setPermissionList(permissionMapper.findPermissionsByRoleId(x.getId())); } } user.setRoleList(roles); return user; } @Override public User findSimpleUserInfoById(int userId) { return userMapper.findById(userId); } @Override public User findSimpleUserInfoByUsername(String username) { return userMapper.findByUsername(username); } }
(5)controller编写
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/find_user") public Object findUserInfo(@RequestParam("username") String username){ return userService.findAllUserInfoByUsername(username); } }
(6)测试
7.4.开发自定义CustomRealm
- 继承 AuthorizingRealm
- 重写 doGetAuthorizationInfo
- 重写 doGetAuthenticationInfo
7.5.ShiroFilterFactoryBean配置
- shiroFilterFactoryBean-》
- SecurityManager-》
- CustomSessionManager
- CustomRealm-》hashedCredentialsMatcher
- SessionManager
- DefaultSessionManager: 默认实现,常用于javase
- ServletContainerSessionManager: web环境
- DefaultWebSessionManager:常用于自定义实现
@Configuration public class ShiroConfig { /** * 设置shiroFilter * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //设置登录成功后访问的路径 shiroFilterFactoryBean.setLoginUrl("/pub/need_login"); //设置登录成功后访问的url shiroFilterFactoryBean.setSuccessUrl("/"); //设置无权限访问的接口,未授权无法访问接口 shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit"); //拦截器路径配置,注意这里一定要用LinkedHashMap,HashMap会出现偶尔无法拦截的情况 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); //设置过滤器 //登出过滤器,用户退出时调用 filterChainDefinitionMap.put("/logout","logout"); //匿名过滤器,游客模式 filterChainDefinitionMap.put("/pub/**","anon"); //登录过滤器,用户只有登录才能访问 filterChainDefinitionMap.put("/authc/**","authc"); //管理员角色才能访问 filterChainDefinitionMap.put("/admin/**","roles[admin]"); //指定权限才能访问 filterChainDefinitionMap.put("/video/update","perms[video_update]"); filterChainDefinitionMap.put("/**","authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 注入securityManager,设置Realm和SessionManager * @return */ @Bean public SecurityManager securityManager(){ DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setRealm(customRealm()); //注意如果不是前后端分离的项目就不需要设置sessionManager securityManager.setSessionManager(customSessionManager()); return securityManager; } /** * 设置散列算法 * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //设设置散列算法,md5 hashedCredentialsMatcher.setHashAlgorithmName("md5"); //设置散列次数 hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } /** * 注入自定义的Realm * @return */ @Bean public CustomRealm customRealm(){ CustomRealm customRealm = new CustomRealm(); customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return customRealm; } /** * 注入自定义的SessionManager * @return */ @Bean public CustomSessionManager customSessionManager(){ CustomSessionManager customSessionManager = new CustomSessionManager(); //超时时间,默认30分钟不操作就会过期,单位豪秒 customSessionManager.setGlobalSessionTimeout(20000); return customSessionManager; } }
7.6.自定义SessionManager验证
public class CustomSessionManager extends DefaultWebSessionManager { private static final String TOKEN="token"; //调用父类构造方法,以防后续有人修改构造,空构造覆盖,会出问题 public CustomSessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String sessionId = WebUtils.toHttp(request).getHeader(TOKEN); //如果sessionId不为空,就调用自定义的逻辑,如果为空就调用父类的方法 if(sessionId!=null){ //调用shiro内部的校验,检测sessionId是否存在,是否过期 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; }else{ return super.getSessionId(request,response); } } }
7.7.API拦截验证案例
(1)AdminController
@RestController @RequestMapping("/admin") public class AdminController { @RequestMapping("/order") public JsonData findOrder(){ Map<String,String> recordMap = new HashMap<>(); recordMap.put("SpringBoot入门到高级实战","300元"); recordMap.put("Cloud微服务入门到高级实战","877元"); recordMap.put("分布式缓存Redis","990元"); return JsonData.buildSuccess(recordMap); } }
(2)LogoutController
@RestController public class LogoutController { @RequestMapping("/logout") public JsonData logout(){ String username = (String)SecurityUtils.getSubject().getPrincipal(); if(username!=null){ SecurityUtils.getSubject().logout(); return JsonData.buildSuccess("logout成功"); } return JsonData.buildError("logout失败"); } }
(3)OrderController
@RestController @RequestMapping("/authc") public class OrderController { @RequestMapping("/save") public JsonData findMyPlayRecord(){ Map<String ,String> recordMap = new HashMap<>(); recordMap.put("SpringBoot入门到高级实战","第8章第1集"); recordMap.put("Cloud微服务入门到高级实战","第4章第10集"); recordMap.put("分布式缓存Redis","第10章第3集"); return JsonData.buildSuccess(recordMap); } }
(4)pubController
@RestController @RequestMapping("/pub") public class PubController { @RequestMapping("/need_login") public JsonData needLogin(){ return JsonData.buildSuccess("温馨提示:请使用对应的账号登录",-2); } @RequestMapping("not_permit") public JsonData notPermit(){ return JsonData.buildSuccess("温馨提示:拒绝访问,没权限",-3); } @RequestMapping("/index") public JsonData index(){ List<String> videoList = new ArrayList<>(); videoList.add("Mysql零基础入门到实战 数据库教程"); videoList.add("Redis高并发高可用集群百万级秒杀实战"); videoList.add("Zookeeper+Dubbo视频教程 微服务教程分布式教程"); videoList.add("2019年新版本RocketMQ4.X教程消息队列教程"); videoList.add("微服务SpringCloud+Docker入门到高级实战"); return JsonData.buildSuccess(videoList); } @PostMapping("/login") public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response){ Subject subject = SecurityUtils.getSubject(); System.out.println("userQuery:"+userQuery); Map<String,Object> info = new HashMap<>(); try { UsernamePasswordToken token = new UsernamePasswordToken(userQuery.getUsername(),userQuery.getPwd()); subject.login(token); info.put("session_id",subject.getSession().getId()); return JsonData.buildSuccess(info,"登录成功"); }catch (Exception e){ info.put("session_id",subject.getSession().getId()); return JsonData.buildError("账号或密码错误"); } } }
(5)VideoController
@RestController @RequestMapping("/video") public class VideoController { @RequestMapping("/update") public JsonData updateVideo(){ return JsonData.buildSuccess("video 更新成功"); } }
(6)用户角色权限分配图
(7)测试
7.8.Shiro密码加密处理
@SpringBootTest class RbacShiroApplicationTests { @Test void contextLoads() { //加密算法 String hashName = "md5"; //密码明文 String pwd = "123456"; //加密函数,使用shiro自带的 SimpleHash hash = new SimpleHash(hashName, pwd, null, 2); System.out.println(hash); } }