開發實戰|第三篇:基於shiro實現權限控制

1.簡介

Apache Shiro是Java的一個安全框架,可用於用認證,授權,加密,會話管理等多個方面,其基本功能點如下圖所示:

在這裏插入圖片描述

模塊 用途
Authenication 身份認證/登錄,驗證用戶是否擁有相應身份
Authorization 授權,權限驗證,驗證某個用戶是否擁有某個權限
Session Manager 會話管理
Cryptography 加密
Web Support web支持
Caching 緩存
Concurrency 支持多線程應用併發驗證

Shiro工作流程如下圖所示:

在這裏插入圖片描述

應用程序交互主體爲Subject,其中Subject可以看做門面,接收請求後實際交給SecurityManager進行處理,SecurityManager爲具體驗證邏輯,而Realm爲數據源來源,可以看做db

2.SpringBoot整合Shiro

  • 數據庫準備

    -- 用戶token表
    create table shiro.user_token
    (
        user_id     bigint       not null
        primary key,
        token       varchar(100) not null comment 'token',
        expire_time datetime     null comment '過期時間',
        update_time datetime     null comment '更新時間',
        constraint token
        unique (token)
    )
    
    -- 用戶表
    create table shiro.user
    (
        user_id        bigint auto_increment
            primary key,
        user_name      varchar(32)                        not null comment '用戶名',
        password       varchar(64)                        not null comment '用戶密碼',
        mobile         varchar(32)                        null comment '手機號',
        salt           varchar(64)                        null comment '鹽',
        locked         smallint(6)                        null comment '是否被鎖定(0:鎖定,1:正常)',
        create_user_id bigint                             null comment '創建人',
        create_time    datetime default CURRENT_TIMESTAMP null comment '創建時間',
        email          varchar(32)                        null,
        constraint user_user_name_uindex
            unique (user_name)
    )
        comment '用戶表';
     
    -- 角色表
    create table shiro.role
    (
        role_id        bigint auto_increment comment '權限id'
            primary key,
        role_name      varchar(32)                        not null,
        remark         varchar(64)                        null comment '權限說明',
        create_user_id bigint                             not null comment '創建者id',
        create_time    datetime default CURRENT_TIMESTAMP null comment '創建時間 '
    )
        comment '權限表';
      
    -- 用戶角色表
    create table shiro.user_role
    (
        id      bigint auto_increment
            primary key,
        user_id bigint not null,
        role_id bigint not null
    )
        comment '用戶權限中間表';
    
    -- 菜單表
    create table shiro.menu
    (
        menu_id     bigint auto_increment
            primary key,
        name        varchar(32) not null comment '菜單名',
        parent_id   bigint      not null comment '父菜單id,頂級菜單爲0',
        perms       varchar(32) null comment '權限集合,多個以","分割',
        parent_name varchar(32) null
    )
        comment '菜單表';
    
    -- 用戶菜單表
    create table shiro.role_menu
    (
        id      bigint auto_increment
            primary key,
        menu_id bigint not null,
        role_id bigint not null
    )@Configuration
    public class FilterConfig {
    
      @Bean
      public FilterRegistrationBean shiroFilterRegistration(){
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        //該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理
        registration.addInitParameter("targetFilterLifecycle", "true");
        registration.setEnabled(true);
        registration.setOrder(Integer.MAX_VALUE - 1);
        registration.addUrlPatterns("/*");
        return registration;
      }
    }
        comment '權限菜單表';
    
    
  • 數據準備

    -- 創建初始用戶
    INSERT INTO shiro.user (user_id, user_name, password, mobile, salt, locked, create_user_id, create_time, email) VALUES (1, 'admin', 'da73d08539c91cdf2f11da4f6dcc5dbc2dd9dc8e1647cf1520e3540e77dc2c3c', '15123625205', '932c2d40-ea59-11e9-a2ad-0235d2b38928', 1, 1, '2019-10-09 05:59:51', null);
    
    -- 創建菜單權限
    INSERT INTO shiro.menu (menu_id, name, parent_id, perms, parent_name) VALUES (1, '新增', 2, 'sys:user:save,sys:role:select', null);
    
  • springboot環境搭建

    springboot基本搭建很簡單,在此就不在搭建,只列出所需的相關依賴:spring-boot-starter-aop,lombok,druid-spring-boot-starter,mysql-connector-java,mybatis-plus-boot-starter,joda-time,cos_api

  • 引入shiro依賴

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    
  • shiro相關配置

    (1) shiroConfig:主要用於設置securityManager及請求攔攔截配置過濾器

    @Configuration
    public class ShiroConfig {
    
        @Bean("securityManager")
        public SecurityManager securityManager(ShiroRealm shiroRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm);
            securityManager.setRememberMeManager(null);
            return securityManager;
        }
    
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
    
            //shiro過濾
            Map<String, Filter> filters = new HashMap<>();
            filters.put("shiro", new ShiroFilter());
            shiroFilter.setFilters(filters);
    
            Map<String, String> filterMap = new LinkedHashMap<>();
            filterMap.put("/webjars/**", "anon");
            filterMap.put("/druid/**", "anon");
            filterMap.put("/app/**", "anon");
            filterMap.put("/sys/login", "anon");
            filterMap.put("/swagger/**", "anon");
            filterMap.put("/v2/api-docs", "anon");
            filterMap.put("/swagger-ui.html", "anon");
            filterMap.put("/swagger-resources/**", "anon");
            filterMap.put("/captcha.jpg", "anon");
            filterMap.put("/aaa.txt", "anon");
            filterMap.put("/**", "shiro");
            shiroFilter.setFilterChainDefinitionMap(filterMap);
    
            return shiroFilter;
        }
    
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
    }
    

    (2) shiroFilter:自定義過濾器

    該過濾器主要定義了請求成功,或登錄失敗後的處理措施

    public class ShiroFilter extends AuthenticatingFilter {
    
      @Override
      protected AuthenticationToken createToken(ServletRequest request,
          ServletResponse response) throws Exception {
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isEmpty(token)) {
          return null;
        }
    
        return new ShiroToken(token);
      }
    
      private String getRequestToken(HttpServletRequest request) {
    
        //從header中獲取token
        String token = request.getHeader("token");
    
        //如果header中不存在token,則從參數中獲取token
        if (StringUtils.isEmpty(token)) {
          token = request.getParameter("token");
        }
        return token;
      }
    
      @Override
      protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
          Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
          return true;
        }
        return false;
      }
    
      @Override
      protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse)
          throws Exception {
        String token = getRequestToken((HttpServletRequest) servletRequest);
        if (StringUtils.isEmpty(token)) {
          HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
          httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
          httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
          String json = new Gson().toJson(ExtResult.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
    
          httpResponse.getWriter().print(json);
    
          return false;
        }
        return executeLogin(servletRequest, servletResponse);
      }
    
      @Override
      protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
          ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
          //處理登錄失敗的異常
          Throwable throwable = e.getCause() == null ? e : e.getCause();
          ExtResult r = ExtResult.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
    
          String json = new Gson().toJson(r);
          httpResponse.getWriter().print(json);
        } catch (IOException e1) {
    
        }
    
        return false;
      }
    }
    

    (3)shiorToken:用於自定義token,後續所有請求均依賴於token校驗

    public class ShiroToken implements AuthenticationToken {
    
      private String token;
    
      public ShiroToken(String token) {
        this.token = token;
      }
    
      @Override
      public Object getPrincipal() {
        return token;
      }
    
      @Override
      public Object getCredentials() {
        return token;
      }
    }
    

    (4) token生成器,用於生成token

    public class TokenGenerator {
    
      public static String generateValue() {
        return generateValue(UUID.randomUUID().toString());
      }
    
      public static String generateValue(String param) {
        try {
          //指定算法,初始化對象
          Provider[] providers = Security.getProviders();
          System.out.println(providers);
          MessageDigest md5 = MessageDigest.getInstance("MD5");
          //重置摘要
          md5.reset();
          //處理數據
          md5.update(param.getBytes());
          //調用digest將MessageDigest設置成初始化狀態,digest()只能調用一次,因此要初始化
          byte[] digest = md5.digest();
          return toHexString(digest);
        } catch (Exception e) {
          throw new ExtException("生成token失敗", e);
        }
      }
    
      private static final char[] hexCode = "0123456789abcdef".toCharArray();
    
      private static String toHexString(byte[] data) {
        if(Objects.isNull(data)){
          return null;
        }
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte datum : data) {
          //對每個數字做位運算,只有都爲1時結果才爲1,否則爲0,然後取出數組中對應下標的數
          sb.append(hexCode[(datum>>4)& 0xF]);
          sb.append(hexCode[datum& 0xF]);
        }
        return sb.toString();
      }
    
      public static void main(String[] args) {
        generateValue();
      }
    }
    

    (5)shiroRealm:自定義realm,即通過該realm實現用戶的認證及授權

    @Component
    public class ShiroRealm extends AuthorizingRealm {
    
      @Resource
      private ShiroService shiroService;
    
      @Override
      public boolean supports(AuthenticationToken token) {
        return token instanceof ShiroToken;
      }
    
      /**
       * @description:用戶授權 <br>
       * @date: 19-10-9 上午11:43
       */
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //獲取用戶信息
        UserDO userDO = (UserDO) principals.getPrimaryPrincipal();
        //獲取用戶權限列表
        Set<String> userPermission = shiroService.getUserPermission(userDO.getUserId());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(userPermission);
        return info;
      }
    
      /**
       * @description:用戶認證 <br>
       * @date: 19-10-9 上午11:43
       */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
          throws AuthenticationException {
        //獲取用戶傳入token
        String accessToken = (String) token.getPrincipal();
        //根據token查詢用戶信息
        UserTokenDO userTokenDO = shiroService.queryByToken(accessToken);
        if (Objects.isNull(userTokenDO) || userTokenDO.getExpireTime().getTime() < System
            .currentTimeMillis()) {
          throw new IncorrectCredentialsException("token失效,請重新登錄");
        }
        //根據用戶id查詢用戶
        UserDO userDO = shiroService.queryUser(userTokenDO.getUserId());
        if(Objects.equals(userDO.getLocked(), 0)){
          throw new LockedAccountException("賬號被鎖定,請聯繫管理員");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDO, accessToken, getName());
        return info;
      }
    }
    

    (5)配置整體過濾器

    @Configuration
    public class FilterConfig {
    
      @Bean
      public FilterRegistrationBean shiroFilterRegistration(){
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        //該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理
        registration.addInitParameter("targetFilterLifecycle", "true");
        registration.setEnabled(true);
        registration.setOrder(Integer.MAX_VALUE - 1);
        registration.addUrlPatterns("/*");
        return registration;
      }
    }
    
  • 測試校驗

    (1)驗證用戶登錄,創建token

    //實體類
    @Data
    public class LoginForm {
    
      private String username;
      private String password;
      private String uuid;
    }
    //請求controller
    @PostMapping("/sys/login")
      public Map<String, Object> login(@RequestBody LoginForm form) throws IOException {
        //校驗驗證碼,省略
    
        //賬號密碼校驗
        UserDO user = userService.queryByUserName(form.getUsername());
    
        if (Objects.isNull(user) || !Objects
            .equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex(), user.getPassword())) {
          return ExtResult.error("賬戶或密碼不正確");
        }
    
        //賬號鎖定
        if (Objects.equals(user.getLocked(), 0)) {
          return ExtResult.error("賬號已被鎖定,請聯繫管理員");
        }
    
        //生成token,併入庫
        ExtResult r = userTokenService.createToken(user.getUserId());
        return r;
      }
    
    }
    
    -----------------------------------------------------------------------------
    //生成token主要邏輯
      private final static int EXPIRE = 3600 * 12;
      @Override
      @Transactional
      public ExtResult createToken(long userId) {
        //生成一個token
        String token = TokenGenerator.generateValue();
        Date now = new Date();
        Date exprise = new Date(now.getTime() + EXPIRE * 1000);
        UserTokenDO userTokenDO = this.getById(userId);
        //如果token不存在,則生成新的token
        if(Objects.isNull(userTokenDO)){
          userTokenDO = new UserTokenDO();
          userTokenDO.setUserId(userId);
          userTokenDO.setToken(token);
          userTokenDO.setExpireTime(exprise);
          userTokenDO.setUpdateTime(now);
          this.saveOrUpdate(userTokenDO);
        }
        //如果存在,則更新token,並更新過期時間
        else{
          userTokenDO.setUpdateTime(now);
          userTokenDO.setExpireTime(exprise);
          userTokenDO.setToken(token);
          this.updateById(userTokenDO);
        }
        return ExtResult.ok().put("token", token).put("exprise", exprise);
      }
    

    (2)新增用戶

    @PostMapping("/save")
    @RequiresPermissions("sys:user:save")
    public ExtResult save(@RequestBody UserDO user){
        ValidatorUtils.validateEntity(user, AddGroup.class);
        user.setCreateUserId(getUserId());
        //密碼加鹽
        user.setPassword(MD5Utils.encrypt(user.getPassword(), user.getSalt()));
        userService.save(user);
        return ExtResult.ok();
    }
    -----------------------------------------------------------------------------
    // ValidatorUtils:用於校驗傳入參數
        public static void validateEntity(Object object, Class<?>... groups)
        throws ExtException {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            for(ConstraintViolation<Object> constraint:  constraintViolations){
                msg.append(constraint.getMessage()).append("<br>");
            }
            throw new ExtException(msg.toString());
        }
    }
    

    (3)實現步驟

    • 用戶登錄根據用戶名查詢出用戶信息,校驗密碼正確,校驗賬號狀態是否正常
    • 新建用戶時,傳入當前登錄用戶token,首先進入shiroRealm(doGetAuthenticationInfo)校驗用戶是否登錄,token是否有效,如果校驗成功進入下一步
    • 上一步校驗成功後,進入回調方法(doGetAuthorizationInfo),根據傳入用戶查詢該用戶權限列表,如果權限列表中包含@RequiresPermissions(“sys:user:save”),指定的權限,則繼續執行後續操作。

    3.參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章