• 2021-07-09 springboot 整合shiro(前后端分离解决方案)


    springboot 整合shiro(前后端分离解决方案)

    前言
    网上大多数关于springboot整合shiro的教程是非前后端分离的,认证、授权失败的处理都是页面跳转,不是返回json数据,本文旨在解决这一问题。

    目标

    • 登录失败,不跳转页面,返回 Json 字符串 {code:xxx, msg:xxx}
    • 权限不足时,不跳转页面,返回 Json 字符串 {code:xxx, msg:xxx}
    • 跳转到哪里去,由前端自己判断

    怎么做

    创建springboot项目,引入依赖 pox.xml

    • pom.xml 只copy了重要的部分出来,spring-boot-starter-web、lombok(辅助工具,个人习惯)、spring-boot-starter-aop(不要忘了)、shiro-spring

      
          
              org.springframework.boot
              spring-boot-starter-web
          
      
          
              org.projectlombok
              lombok
              true
          
          
          
              org.springframework.boot
              spring-boot-starter-aop
          
          
              org.apache.shiro
              shiro-spring
              1.5.3
          
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

    创建用户实体类 Accout.java

    • 实际使用中应使用Mybatis或JPA与数据库数据对应,本案例简化使用内存写死两个用户做说明

    • 注解都是lombok,为了不用写Get Set 方法,与本文要解决的问题非强相关

    • Accout.java 实际案例中用户的permission role应为多个,即使用Map存储多个,此处简化处理

      @Getter
      @Setter
      @AllArgsConstructor
      public class Accout {
      private String name;
      private String password;
      private String permis;
      private String roles;
      }

    创建查找用户服务 AccoutService.java AccoutServiceImp.java

    • 创建接口,通过用户名返回用户

    • 创建service类实现接口,此处写死了两个用户,user1 user2,分别带有不同权限

      public interface AccoutService {
      public Accout checkuser(String username);
      }
      /文件分割/
      @Service
      public class AccoutServiceImp implements AccoutService{
      @Override
      public Accout checkuser(String username) {
      Accout user1 = new Accout(“user1”,“123456”,“permis1”,“role1”);
      Accout user2 = new Accout(“user2”,“123456”,“permis2”,“role2”);

          if(username.equals("user1")){return user1;}
          if(username.equals("user2")){return user2;}
          return null;
      }
      
      • 1
      • 2
      • 3
      • 4

      }

    封装返回结果 ResponseFactory.java ResponseFactoryImp.java

    • 创建接口,向前端返回JSON数据

    • 创建service类实现接口

      public interface ResponseFactory {
      void makeResponse(HttpServletResponse res, String code, String msg) throws IOException;
      }
      /文件分割/
      @Service
      public class ResponseFactoryImp implements ResponseFactory {
      @Override
      public void makeResponse(HttpServletResponse res, String code, String msg) throws IOException {
      res.setContentType(“application/json; charset=utf-8”);
      Map result = new HashMap();
      result.put(“code”, code);
      result.put(“msg”, msg);
      res.getWriter().write(result.toString());
      }
      }

    重写 AuthorizingRealm 中认证和授权的方法 AccoutRealm.java

    • 调用写好的用户服务,通过用户名获取用户

    • doGetAuthorizationInfo 用于认证通过的用户授予权限

    • doGetAuthenticationInfo 用于对用户进行认证,是否合法用户

      public class AccoutRealm extends AuthorizingRealm {
      @Autowired
      private AccoutServiceImp accoutServiceImp;

      //授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
          //拿到当前用户,认证成功时候放进来的。
          Subject subject = SecurityUtils.getSubject();
          Accout accout = (Accout) subject.getPrincipal();
          if(accout != null){
              //把用户里有的权限都给他赋予上
              Set roles = new HashSet<>();
              roles.add(accout.getRoles());
              SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
              info.addStringPermission(accout.getPermis());
              return info;
          }
          return null;
      }
      
      //认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
          //从token中获取用户名,取出用户
          UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
          Accout accout = accoutServiceImp.checkuser(token.getUsername());
          if(accout != null)
          {
              //没有传token进去,不知道如何进行密码比对的,可以打断点自己分析下
              return new SimpleAuthenticationInfo(accout,accout.getPassword(),getName());
          }
          return null;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30

      }

    shiro配置,注入自己写的AccoutRealm Shiroconfig.java

    • 主要是把自己写的 Realm注入

      @Configuration
      public class ShiroConfig{

      //把注入了自己的Realm的defaultWebSecurityManager注入ShiroFilterFactoryBean
      @Bean
      public ShiroFilterFactoryBean shiroFilterFactoryBean(){
          ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
          shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager());
          return shiroFilterFactoryBean;
      }
      
      //把自己写的Realm注入defaultWebSecurityManager
      @Bean
      public DefaultWebSecurityManager defaultWebSecurityManager(){
          DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
          manager.setRealm(accoutRealm());
          return manager;
      }
      
      //把自己写的Realm放入spring容器中
      @Bean
      public AccoutRealm accoutRealm(){
          return new AccoutRealm();
      }
      
      //不加这一段,不执行doGetAuthorizationInfo 授权,不知道为什么
      @Bean
      public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
          AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
          authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
          return authorizationAttributeSourceAdvisor;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29

      }

    编写controller AccoutController.java

    • login需要采用post方式发送username password到后端,注意subject.login会调用doGetAuthenticationInfo认证,产生的异常我们统一另作处理即可

    • manage只由拥有 permis1 用户权限才可调用

      @RestController
      public class AccoutController {

      @RequestMapping(value="/login",method = RequestMethod.POST)
      public String login(@RequestParam("username") String username,@RequestParam("password") String password){
          Subject subject = SecurityUtils.getSubject();
          UsernamePasswordToken token = new UsernamePasswordToken(username, password);
          subject.login(token);
          return "{code:200,msg:login success!";
      }
      
      @GetMapping("/manage")
      @RequiresPermissions("permis1")
      public String manage(){
          return "manage page!";
      }
      
      @GetMapping("/administrator")
      public String administrator(){
          return "administrator page!";
      }
      
      @GetMapping("/main")
      public String mainpage(){
          return "main page!";
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

      }

    统一处理认证授权失败 AuthException.java

    @ControllerAdvice
    public class AuthException {
        @Autowired
        private ResponseFactoryImp myresponse;
    
        //异常未完全列举
        @ExceptionHandler(value = UnauthorizedException.class)//处理访问方法时权限不足问题
        public void AuthcErrorHandler(HttpServletResponse res, Exception e) throws IOException {
            myresponse.makeResponse(res,"1","权限不足!"+e.toString());
        }
    
        @ExceptionHandler(value = UnknownAccountException.class) //处理未知账号
        public void UnKnowAccountErrorHandler(HttpServletResponse res, Exception e) throws IOException {
            myresponse.makeResponse(res,"2","未知账号!"+e.toString());
        }
    
        @ExceptionHandler(value = IncorrectCredentialsException.class) //处理账号凭证异常
        public void IncorrectCredentialErrorHandler(HttpServletResponse res, Exception e) throws IOException {
            myresponse.makeResponse(res,"3","凭证异常!"+e.toString());
        }
    
        @ExceptionHandler(value = AuthorizationException.class) //处理账号未登录
        public void AuthorizationErrorHandler(HttpServletResponse res, Exception e) throws IOException {
            myresponse.makeResponse(res,"4","请登录后访问!"+e.toString());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    整个项目树形结构

    ─shiro
        │  ShiroApplication.java //启动类
        │
        ├─config
        │      AuthException.java
        │      ShiroConfig.java
        │
        ├─controller
        │      AccoutController.java
        │
        ├─Entity
        │      Accout.java
        │
        ├─MyResPonse
        │      ResponseFactory.java
        │      ResponseFactoryImp.java
        │
        ├─realm
        │      AccoutRealm.java
        │
        └─service
                AccoutService.java
                AccoutServiceImp.java
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用Postman测试:

    • 未登录直接访问不需要权限的方法
      未登录直接访问不需要权限的方法

    • 未登录直接访问需要权限的方法
      在这里插入图片描述

    • 访问登录方法,输入不存在的用户
      在这里插入图片描述

    • 访问登录方法,输入存在的用户,密码错误
      在这里插入图片描述

    • 访问登录方法,输入存在的用户,且密码正确
      在这里插入图片描述

    • 登录后访问当前用户没有权限的方法
      在这里插入图片描述

    • 登录后访问不需要权限的方法
      在这里插入图片描述

    • 登录后访问当前用户有权限的方法
      在这里插入图片描述

    结语

    解决了springboot整合shiro 在前后端分离场景的应用,但对于 登录时限、单点登录等功能限制还需继续完善。

    附shiro常见需要拦截处理的异常,上述案例中未全部处理,不同版本有变化,自行百度

    • 1.shiro的常见异常
      • 1.1 AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。 AuthenticationException包含以下子类:

        • 1.1.1 CredentitalsException 凭证异常
          • IncorrectCredentialsException 不正确的凭证
          • ExpiredCredentialsException 凭证过期
        • 1.1.2 AccountException 账号异常
          • ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
          • UnknownAccountException 未知的账号
          • ExcessiveAttemptsException 认证次数超过限制
          • DisabledAccountException 禁用的账号
          • LockedAccountException 账号被锁定
          • pportedTokenException 使用了不支持的Token
      • 1.2AuthorizationException 权限校验子类:

        • 1.2.1 UnauthorizedException: 抛出以指示请求的操作或对请求的资源的访问是不允许的。
        • 1.2.2 UnanthenticatedException:当尚未完成成功认证时,尝试执行授权操作时引发异常。
  • 相关阅读:
    MATLAB程序设计:牛顿迭代法
    数据处理任务——知识点总结
    【嵌入式开发 Linux 常用命令系列 8 --代码格式修改工具 astyle】
    安全-js的apply方法
    【Truffle】四、通过Ganache部署连接
    CSDN页面左上角出现红色“不安全 | https” ,并且把鼠标放在上面的头像和消息时无法下拉菜单
    20_数组的常见操作
    51单片机8(LED闪烁)
    法律战爆发:“币安退出俄罗斯引发冲击波“
    Java 函数式编程「一」
  • 原文地址:https://blog.csdn.net/m0_67265464/article/details/126325617