# shiro-spring-boot-demo **Repository Path**: whole-stack-of-white/shiro-spring-boot-demo ## Basic Information - **Project Name**: shiro-spring-boot-demo - **Description**: SpringBoot整合Shiro前后端不分离源码 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-12-12 - **Last Updated**: 2024-05-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot整合Shiro前后端不分离 ## 一、组件说明 ![image-20221212144318500](Pictures/image-20221212144318500.png) 来源于百度图片,三个部分,主题Subject、安全管理器SecurityManager、Realm ### 1.2 Shiro配置的几个过滤器 ```java anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), authcBasic(BasicHttpAuthenticationFilter.class), authcBearer(BearerHttpAuthenticationFilter.class), logout(LogoutFilter.class), noSessionCreation(NoSessionCreationFilter.class), perms(PermissionsAuthorizationFilter.class), port(PortFilter.class), rest(HttpMethodPermissionFilter.class), roles(RolesAuthorizationFilter.class), ssl(SslFilter.class), user(UserFilter.class), invalidRequest(InvalidRequestFilter.class); ``` ## 二、整合步骤 ### 2.1 导入依赖 ~~~xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.projectlombok lombok mysql mysql-connector-java 5.1.47 com.baomidou mybatis-plus-boot-starter 3.5.0 org.apache.shiro shiro-spring-boot-starter 1.8.0 ~~~ ### 2.2 配置文件 ~~~yaml server: port: 2022 spring: # thymeleaf配置 thymeleaf: prefix: classpath:/templates/ suffix: .html encoding: UTF-8 cache: false # 数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///shiro-spring-boot-demo?characterEncoding=utf-8&useSSL=false username: root password: root # mp的配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-aliases-package: com.cxs.model mapper-locations: classpath:mapper/*.xml global-config: banner: false ~~~ ### 2.3 编写认证Realm AuthorizingRealm有两个方法需要我们重写 - doGetAuthenticationInfo:自定义认证 根据用户输入的用户名查数据库,存在将其封装为一个AuthenticationInfo对象返回,不存在自己处理,这里抛出UnknownAccountException异常,这个异常会在异常处理器处理(后面提) AuthenticationInfo对象中会有一个载荷,实现类SimpleAuthenticationInfo构造方法中的第一个参数,这个载荷用于认证成功后Shiro存储的实体,可以自定义,我这直接将用户信息存里面了,但是存什么,在 subject.getPrincipal()中取出来的就是什么,建议将用户id和用户名存一下 - doGetAuthorizationInfo:自定义授权 根据认证成功后Shiro存的载荷查询用户应有的角色权限,封装到一个AuthorizationInfo对象中,由Shiro的authc过滤器去判断是否有权限,看下其继承结构 ![image-20221212140606334](Pictures/image-20221212140606334.png) ~~~java /* * @Project:spring-boot-shiro-demo * @Author:cxs * @Motto:放下杂念,只为迎接明天更好的自己 * */ public class AuthRealm extends AuthorizingRealm { @Autowired private UserService userService; public AuthRealm(){ // 注入密码加密 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashIterations(1024); matcher.setHashAlgorithmName("MD5"); this.setCredentialsMatcher(matcher); } public static void main(String[] args) { Md5Hash md5Hash = new Md5Hash("user","user",1024); System.out.println(md5Hash.toString()); } /** * 负责认证 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { if (token == null || token.getPrincipal() == null) { throw new UnknownAccountException("用户不存在!"); } // 从 AuthenticationToken 中获取当前用户 String username = (String) token.getPrincipal(); // 查询数据库获取用户信息,此处使用 Map 来模拟数据库 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userService.getOne(wrapper); // 用户不存在 if (user == null) { throw new UnknownAccountException("用户不存在!"); } /** * 将获取到的用户数据封装成 AuthenticationInfo 对象返回,此处封装为 SimpleAuthenticationInfo 对象。 * 参数1. 认证的实体信息,可以是从数据库中获取到的用户实体类对象或者用户名 * 参数2. 查询获取到的登录密码 * 参数3. 盐值 * 参数4. 当前 Realm 对象的名称,直接调用父类的 getName() 方法即可 */ SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); // 设置盐 info.setCredentialsSalt(ByteSource.Util.bytes(user.getUserName())); return info; } /** * 负责权限 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Object principal = principals.getPrimaryPrincipal(); User token = (User) principal; // 这个步骤在认证之后,principal必定不为空 // 根据用户获取对应权限,这里就不获取了,因为权限和用户在一张表里 Set roles = new HashSet<>(); roles.add(token.getRole()); AuthorizationInfo info = new SimpleAuthorizationInfo(roles); return info; } } ~~~ ### 2.4 编写Shiro核心配置类 以下两步配置后不会发生无限重定向,去他的不说了,注释都写了 - shiroFilterFactoryBean.setLoginUrl("/login"); 配置登录页面的地址, - 放行/login路径 ~~~java /* * @Project:spring-boot-shiro-demo * @Author:cxs * @Motto:放下杂念,只为迎接明天更好的自己 * */ @Configuration public class ShiroConfig { /** * 注入realm * @return */ @Bean public AuthRealm authRealm(){ return new AuthRealm(); } /** * 配置 SecurityManager */ @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置Realm securityManager.setRealm(authRealm()); return securityManager; } /** * 配置访问资源需要的权限 */ @Bean ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 指定登录的地址,请勿指定xxx.html,说过了templates下的文件不能通过浏览器直接访问 shiroFilterFactoryBean.setLoginUrl("/login"); // 自定义过滤器 LinkedHashMap filterChainDefinitionMap = new LinkedHashMap(); filterChainDefinitionMap.put("/error", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/auth/login", "anon"); // anno可匿名访问 filterChainDefinitionMap.put("/**", "authc"); // 其他所有资源均需登录才能访问 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } //开启对shior注解的支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager()); return advisor; } } ~~~ ### 2.5 编写接口 登陆接口, User user = (User) subject.getPrincipal(); 这里取出来的值是realm中决定的,不一致或发生类型转换异常(ClassCastException) ~~~java /** * 用户认证接口 * @param request * @param model * @param username * @param password * @return */ @PostMapping("/auth/login") public String authLogin(HttpServletRequest request, Model model, String username, String password){ if (!StringUtils.hasLength(username)) { model.addAttribute("msg", "用户名不能为空"); return "login"; } if (!StringUtils.hasLength(password)) { model.addAttribute("msg", "密码不能为空"); return "login"; } // 获取当前用户主体 Subject subject = SecurityUtils.getSubject(); // 将用户名和密码封装成 UsernamePasswordToken 对象 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username.trim(), password.trim()); // 执行认证流程,走我们的自定义realm subject.login(usernamePasswordToken); // 登录成功,将用户信息存于session HttpSession session = request.getSession(); // 这里获取的载荷是realm中决定的 User user = (User) subject.getPrincipal(); session.setAttribute("user", user); return "index"; } ~~~ 一个列表接口,这个接口只有admin角色才能访问,有两个用户 admin拥有admin角色,user拥有user角色,数据库sql文件中内置了 ~~~java @GetMapping("/list") @RequiresRoles("admin") public String list(Model model){ model.addAttribute("list", userService.list()); return "list"; } ~~~ ### 2.6 统一的异常处理器 为了错误稍微的好看些,配置个异常处理器,主要处理两个异常,一个认证、一个授权,有需要可以自己加 新建了一个error页面,来显示错误 ~~~java /* * @Project:spring-boot-shiro-demo * @Author:cxs * @Motto:放下杂念,只为迎接明天更好的自己 * */ @ControllerAdvice public class ShiroExceptionHandler { /** * 处理用户认证时所抛出的异常 * @param model * @param e * @return */ @ExceptionHandler(value = AuthenticationException.class) public String authExceptionHandle(Model model, AuthenticationException e){ if (e instanceof UnknownAccountException) { // 用户名不存在会抛出这个异常 model.addAttribute("msg", "用户名不存在"); return "error"; } else if (e instanceof CredentialsException) { // 密码验证失败会抛出这个异常 model.addAttribute("msg", "密码验证失败"); return "error"; } else { model.addAttribute("msg", "用户名或密码错误"); return "error"; } } /** * 处理授权时抛出的异常 * @param model * @param e * @return * AuthorizationException 用户无权限访问资源会抛出这个异常 */ @ExceptionHandler(value = AuthorizationException.class) public String forbiddenExceptionHandle(Model model, AuthorizationException e){ model.addAttribute("msg", "用户权限不足,拒绝访问"); return "error"; } } ~~~ ## 三、成果展示 ### 3.1 admin/admin登录 ![image-20221212143900498](Pictures/image-20221212143900498.png) ### 3.2 admin查看用户列表 ![image-20221212143926063](Pictures/image-20221212143926063.png) ### 3.3 user/user登录 ![image-20221212143958876](Pictures/image-20221212143958876.png) ### 3.4 user查看用户列表 ![image-20221212144022641](Pictures/image-20221212144022641.png) ### 3.5 用户名或密码错误 ![image-20221212144042329](Pictures/image-20221212144042329.png) ## 四、源代码地址 码云:https://gitee.com/whole-stack-of-white/shiro-spring-boot-demo