一、Shiro介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
他有三大核心组件:
- Subject
即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。 - SecurityManager
它是Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。 - Realm
Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个 Realm,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。
二、SpringBoot 整合 Shiro
这里就以SpringBoot开发的一个博客项目中的登录模块为例,来展示与Shiro的整合,从而实现登录的认证。
-
1、引入Shiro相关依赖
<!--shiro核心--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.2</version> </dependency> <!--shiro整合spring的包--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.9.0</version> </dependency> <!--shiro整合thymeleaf的包--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
我的SpringBoot 版本是 2.6.4,Shiro版本自行选择。
-
2、login.html
<form class="ui large form" th:action="@{/admin/login}" method="post" > <div class="ui segment"> <div class="field"> <div class="ui left icon input"> <i class="user icon"></i> <input type="text" name="username" placeholder="用户名"> </div> </div> <div class="field"> <div class="ui left icon input"> <i class="lock icon"></i> <input type="password" name="password" placeholder="密码"> </div> </div> <button class="ui fluid large teal submit button"> 登 录 </button> </div> <div class="ui error mini message"></div> <div class="ui mini negative message" th:if="${message!=null}" th:text="${message}"></div> </form>
登录页面表单部分,管理员输入用户名和密码后登录提交到 /admin/login 中去处理。
-
3、LoginController
@Controller @RequestMapping("/admin") public class LoginController { @Autowired UserService userService; //去登录页面 @RequestMapping("/toLogin") public String loginPage(){ return "admin/login"; } //登录 @RequestMapping("/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){ System.out.println("登录...............................\n\n"); System.out.println("登录用户名:" + username); System.out.println("登录密码:" + password); //获取当前用户 Subject subject = SecurityUtils.getSubject(); //封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, MD5Utils.code(password)); try{ subject.login(token);//执行登录的方法,如果没有异常就说明ok return "admin/index"; }catch (UnknownAccountException e){ //用户名不存在 model.addAttribute("message","用户名错误!"); return "admin/login"; }catch (IncorrectCredentialsException e){//密码错误 model.addAttribute("message","密码错误!"); return "admin/login"; } } //无认证 @RequestMapping("/toNoauth") public String Unauthorized(){ return "admin/noauth"; } //logout登出功能 shiro已经实现! }
将传过来的用户名和密码封装成登录数据 UsernamePasswordToken ,并使用shiro的subject组件自带的login方法,进行登录。
编写自定义的Realm,在Realm中主要工作就是授权和认证,连接真实数据库,根据刚封装token中的username查询用户是否存在,如果不存在,则会抛出UnknownAccountException 异常,如果存在,则继续将封装token中的密码与真实数据库中的密码比对,当然,密码校验shiro已经帮我们做了,不用我们自己写,如果比对不成功,则抛出IncorrectCredentialsException 异常,如果用户存在且密码比对成功,则 return “admin/index”; 成功进入首页。
这边还定义了当没有认证直接访问管理员首页及其它页面时,跳转的controller /toNoauth,logout登出功能shiro已经帮我们实现,不需要自己写。
-
4、自定义Realm UserRealm
//自定义的UserRealm public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了 -》 授权 doGetAuthorizationInfo"); return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了 -》 认证 doGetAuthenticationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken)token; System.out.println("userToken -> username" + userToken.getUsername()); System.out.println("userToken -> password" + userToken.getPassword()); //获取数据库中真实用户 User user = userService.queryUserByName(userToken.getUsername()); System.out.println("user ->" + user); if(user == null){ return null; } //把用户存入session Subject subject = SecurityUtils.getSubject(); subject.getSession().setAttribute("user",user); //密码认证,shiro来做 不让你来做 return new SimpleAuthenticationInfo(user,user.getPassword(),""); } }
好了,到这一步,管理员登录认证已经完成,接下来也是最后一步,我们需要编写ShiroConfig配置类来决定哪些请求可以放行,哪些请求需要拦截认证,未认证/授权跳转的页面,登录成功跳转的页面等。
-
5、ShiroConfig
@Configuration public class ShiroConfig { //ShiroFilterFactoryBean :第三步 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //必须设置 SecurityManager 如果不设置就无法完成认证和授权 bean.setSecurityManager(securityManager); //添加Shiro的内置过滤器 /* * anno:无需认证就可以访问 * authc:必须认证才能访问 * user:必须拥有 记住我 功能才能用 * perms:拥有对某个资源的权限才能访问 * role:拥有某个角色权限 才能访问 * */ Map<String,String> filterMap = new LinkedHashMap<>(); // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 // logout shiro定义好的过滤器名字 /logout访问路径 // 浏览器访问的地址栏路径中以/logout结尾的路径 走logout过滤器 // logout会清除session 退出登录 filterMap.put("/admin/logout", "logout"); //所有的img js css文件走 anon过滤器 此过滤器代表放过拦截 不需要权限也能访问 filterMap.put("/css/**", "anon"); filterMap.put("/images/**", "anon"); filterMap.put("/js/**", "anon"); //放过登录页面拦截 filterMap.put("/admin/toLogin", "anon"); filterMap.put("/admin/login", "anon"); // **代表所有路径 除以上路径外的管理员页面都拦截 filterMap.put("/admin/**", "authc"); //设置未认证的请求 bean.setLoginUrl("/admin/toNoauth"); bean.setFilterChainDefinitionMap(filterMap); //登录成功跳转的页面 bean.setSuccessUrl("admin/index"); //设置未授权的请求 bean.setUnauthorizedUrl("/admin/toNoauth"); return bean; } //DefaultWebSecurityManager :第二步 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联UserRealm securityManager.setRealm(userRealm); return securityManager; } //创建realm对象,需要自定义对象 :第一步 @Bean(name = "userRealm") public UserRealm userRealm(){ return new UserRealm(); } //整合ShiroDialect,用于整合shiro thymeleaf @Bean public ShiroDialect getShiroDialect() { return new ShiroDialect(); } }
-
6、测试
(1) 登录页面
(2)当没有登录认证,直接访问管理员首页
(3)当用户不存在
(4)当密码错误
(5)当用户名和密码正确
(6)登录成功后访问管理员首页
(7)登出
(8)登出后再访问管理员首页