2

一、前言

diboot iam-base是一款基于shiro安全框架二次开发的应用于PC WEB的前后端分离的认证授权框架,所以服务端的认证信息依然使用有状态管理,即session存储.
但是最近发现有的小伙伴将diboot-iam用在移动端,我们知道移动端是没有session,此时我们就需要对diboot-iam进行一点微小的改造,让其进入无状态管理,从而适应于移动端认证。
注:本课程基于diboot2.1.2版本

diboot2.2.0后无状态已经内置StatelessJwtAuthFilter,直接使用即可

二、你可以学到?

  • shiro无状态改写
  • 如何基于iam-base的扩展其他登录方式
  • 如何替换框架中默认的iam_user用户类型

三、代码

业务场景:小程序登陆,改写原始的用户名密码登陆,基于微信用户的openid,无密登陆

3.1 无状态配置

  • 基于diboot-iam的DefaultJwtAuthFilter, 创建新的shiro过滤器,并进行token相关的调整

    /**
     * 认证过滤器
     * @author : uu
     * @version : v1.0
     * @Date 2020/10/27  19:34
     */
    @Slf4j
    public class StatelessJwtAuthFilter extends DefaultJwtAuthFilter {
    
      /**
       * 判断是否登录
       * 这里逻辑只是进行简单的修改,增加了refreshtoken(这里我设置生成逻辑和token生成逻辑内容相同,但是该token永久有效),可以选择其他方式
       * <br/>
       * 大致逻辑为:token没过期,使用token;
       *           token即将过期,使用token换新的token;
       *           token已经过期,使用refreshtoken换新的token
       *           最后拿到可用的token,进行登陆(登陆这里还是使用原来的realname,可以查看BaseJwtRealm逻辑,根据需要使用缓存登陆,或者直接查询数据库)
       * 
       * 返回true则直接进入控制器
       * @param request
       * @param response
       * @param mappedValue
       * @return
       */
      @Override
      protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
          HttpServletRequest httpRequest = (HttpServletRequest)request;
          // OPTIONS类型的请求需要拦截处理(否则抛出异常),让系统执行真正的请求
          if (HttpMethod.OPTIONS.matches(httpRequest.getMethod())) {
              return true;
          }
          // 设置当前token
          String currentToken = JwtUtils.getRequestToken(httpRequest);
          if (V.isEmpty(currentToken)) {
              // 当前token验证失败
              return false;
          }
          boolean needRefreshToken = false;
          Claims claims = JwtUtils.getClaimsFromRequest(httpRequest);
          // 尝试使用refreshClaims替换
          if (V.isEmpty(claims)) {
              claims = getClaimsFromRequestByRefreshtoken(httpRequest);
              needRefreshToken = true;
          }
          if (V.isEmpty(claims)) {
              return false;
          } else if (V.notEmpty(claims.getSubject())) {
              // 判断是否需要借助refreshtoken 置换token
              if (needRefreshToken) {
                  log.debug("refreshToken验证成功!account={}", claims.getSubject());
                  currentToken = JwtUtils.generateToken(claims.getSubject(), (long)JwtUtils.EXPIRES_IN_MINUTES);
                  JwtUtils.addTokenToResponseHeader((HttpServletResponse)response, currentToken);
              } else {
                  log.debug("Token验证成功!account={}", claims.getSubject());
                  // 如果当前token有效,如果在指定时间内可以通过当前token进行置换新的token
                  String newToken = JwtUtils.generateNewTokenIfRequired(claims);
                  if (V.notEmpty(newToken)) {
                      JwtUtils.addTokenToResponseHeader((HttpServletResponse)response, currentToken);
                      currentToken = newToken;
                  }
              }
              // 构建登陆的token
              BaseJwtAuthToken baseJwtAuthToken = new BaseJwtAuthToken();
              baseJwtAuthToken.setAuthAccount(S.substringBefore(claims.getSubject(), ","));
              baseJwtAuthToken.setAuthtoken(currentToken);
              baseJwtAuthToken.setAuthType(Cons.DICTCODE_AUTH_TYPE.WX_MP.name());
              baseJwtAuthToken.setUserTypeClass(WxMember.class);
              IamSecurityUtils.getSubject().login(baseJwtAuthToken);
              return true;
          } else {
              log.debug("Token验证失败!");
              return false;
          }
      }
    
      /**
       * 没有登录的情况下会走此方法
       * @param request
       * @param response
       * @return
       * @throws Exception
       */
      @Override
      protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
          log.debug("Token认证失败: onAccessDenied");
          JsonResult jsonResult = new JsonResult(Status.FAIL_INVALID_TOKEN);
          this.responseJson((HttpServletResponse)response, jsonResult);
          return false;
      }
    
    
      /**
       * 获取refreshtoken的Claims
       * @param request
       * @return
       */
      private Claims getClaimsFromRequestByRefreshtoken(HttpServletRequest request) {
          String authHeader = request.getHeader("refreshtoken");
          String refreshtoken = null;
          if (authHeader != null) {
              refreshtoken = authHeader.startsWith("Bearer ") ? authHeader.substring("Bearer ".length()) : authHeader.trim();
          }
          if (V.isEmpty(refreshtoken)) {
              log.warn("refreshtoken 为空!url={}", request.getRequestURL());
              return null;
          } else {
              try {
                  return (Claims) Jwts.parser().setSigningKey(JwtUtils.SIGN_KEY).parseClaimsJws(refreshtoken).getBody();
              } catch (ExpiredJwtException var3) {
                  log.info("refreshtoken已过期:{}", refreshtoken);
              } catch (Exception var4) {
                  log.warn("refreshtoken解析异常", var4);
              }
    
              return null;
          }
      }
  • 禁用shiro的session

    /**
     * 禁用session
     *
     * @author : uu
     * @version : v1.0
     * @Date 2020/10/28  11:06
     */
    public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
    
      @Override
      public Subject createSubject(SubjectContext context) {
          //不创建session
          context.setSessionCreationEnabled(false);
          return super.createSubject(context);
      }
    }
  • 增加ShiroConfig.java,重新改写diboot-iam的ShiroFilterFactoryBean定义(此处可以直接复制IamBaseAutoConfig#shiroFilterFactoryBean来改动哦)

    /**
     * 无状态shiro配置
     *
     * @author : uu
     * @version : v1.0
     * @Date 2020/10/27  19:33
     */
    @Configuration
    public class StatelessShiroConfig {
    
      @Autowired
      private IamBaseProperties iamBaseProperties;
    
      @Bean
      protected ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {
          ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
          Map<String, Filter> filters = new LinkedHashMap();
          // ---------更改部分  start-------------
          // 设置无状态的过滤器 详细见:StatelessJwtAuthFilter,替换 diboot提供的 DefaultJwtAuthFilter
          filters.put("jwt", new StatelessJwtAuthFilter());
          shiroFilterFactoryBean.setFilters(filters);
          // 设置无状态session,SessionsSecurityManager默认使用DefaultWebSecurityManager实现,这里需要设置session不创建 见自定义:StatelessDefaultSubjectFactory
          if (securityManager instanceof DefaultWebSecurityManager) {
              DefaultWebSecurityManager defaultWebSecurityManager = ((DefaultWebSecurityManager) securityManager);
              // 设置不创建session
              defaultWebSecurityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());
              // subject禁止存储到session(这里不设置,最后还是会将subject存储)
              //详情见org.apache.shiro.mgt.DefaultSubjectDAO#save
              DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator();
              webEvalutator.setSessionStorageEnabled(false);
              ((DefaultSubjectDAO)defaultWebSecurityManager.getSubjectDAO())
                      .setSessionStorageEvaluator(webEvalutator);
          }
          // ---------更改部分  end-------------
          
          shiroFilterFactoryBean.setSecurityManager(securityManager);
          shiroFilterFactoryBean.setUnauthorizedUrl("/error");
          Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
          filterChainDefinitionMap.put("/static/**", "anon");
          filterChainDefinitionMap.put("/diboot/**", "anon");
          filterChainDefinitionMap.put("/error/**", "anon");
          filterChainDefinitionMap.put("/auth/**", "anon");
          filterChainDefinitionMap.put("/uploadFile/download/*/image", "anon");
          boolean allAnon = false;
          String anonUrls = this.iamBaseProperties.getAnonUrls();
          if (V.notEmpty(anonUrls)) {
              String[] var7 = anonUrls.split(",");
              int var8 = var7.length;
    
              for(int var9 = 0; var9 < var8; ++var9) {
                  String url = var7[var9];
                  filterChainDefinitionMap.put(url, "anon");
                  if (url.equals("/**")) {
                      allAnon = true;
                  }
              }
          }
          filterChainDefinitionMap.put("/login", "authc");
          filterChainDefinitionMap.put("/logout", "logout");
          if (allAnon && !this.iamBaseProperties.isEnablePermissionCheck()) {
              filterChainDefinitionMap.put("/**", "anon");
          } else {
              filterChainDefinitionMap.put("/**", "jwt");
          }
    
          shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
          return shiroFilterFactoryBean;
      }
    }

    至此,我们已经将无状态的shiro改写完毕。接下来我们再看看如何通过iam-base自定义用户登陆

    3.2 自定义登陆和替换用户类型

    注:此处不会涉及小程序前端代码
    iam-base默认采用iam_user为登陆用户的信息表,登陆方式为:用户名-密码,如果上述无法满足你的项目需求,你可以看这里自定义扩展
  • 替换用户为wx_member(微信用户的基本信息表)

    /**
     * 微信登陆凭证(只需要openid登陆,不需要密码)
     *
     * 继承AuthCredential
     * @author : uu
     * @version : v1.0
     * @Date 2020/10/27  17:38
     */
    @Getter
    @Setter
    public class WXMemberCredential extends AuthCredential {
    
      private static final long serialVersionUID = -738269438230072612L;
      /**
       * 微信登陆的标示
       */
      private String openid;
      /**
       * 用户类型
       */
      private Class userTypeClass = WxMember.class;
    
      @Override
      public String getAuthAccount() {
          return this.openid;
      }
    
      @Override
      public String getAuthSecret() {
          return null;
      }
    }
  • 实现AuthService接口,定义认证方式及接口实现(默认提供了一些枚举Cons.DICTCODE_AUTH_TYPE,不够用可以自定义)

    /**
     * 自定微信登陆service
     * 
     * @author : uu
     * @version : v1.0
     * @Date 2020/10/27  19:21
     */
    @Service
    @Slf4j
    public class WXMpAuthServiceImpl implements AuthService {
    
      @Autowired
      private IamAccountService accountService;
    
      @Autowired
      private HttpServletRequest request;
    
      @Autowired
      private IamLoginTraceService iamLoginTraceService;
    
      @Override
      public String getAuthType() {
          
          return Cons.DICTCODE_AUTH_TYPE.WX_MP.name();
      }
    
      @Override
      public IamAccount getAccount(BaseJwtAuthToken jwtToken) throws AuthenticationException {
          // 这里我加了一个本地缓存,先从缓存查询账户是否存在,如果已经存在,那么直接返回
          IamAccount cacheAccount = AccountMemoryCache.get(jwtToken.getAuthAccount());
          if (V.notEmpty(cacheAccount)) {
              return cacheAccount;
          }
          LambdaQueryWrapper<IamAccount> queryWrapper = Wrappers.<IamAccount>lambdaQuery()
                  .eq(IamAccount::getUserType, jwtToken.getUserType())
                  .eq(IamAccount::getAuthType, Cons.DICTCODE_AUTH_TYPE.WX_MP.name())
                  .eq(IamAccount::getAuthAccount, jwtToken.getAuthAccount())
                  .orderByDesc(BaseEntity::getId);
          IamAccount latestAccount = this.accountService.getSingleEntity(queryWrapper);
          if (latestAccount == null) {
              return null;
          } else if (Cons.DICTCODE_ACCOUNT_STATUS.I.name().equals(latestAccount.getStatus())) {
              throw new AuthenticationException("用户账号已禁用! account=" + jwtToken.getAuthAccount());
          } else if (Cons.DICTCODE_ACCOUNT_STATUS.L.name().equals(latestAccount.getStatus())) {
              throw new AuthenticationException("用户账号已锁定! account=" + jwtToken.getAuthAccount());
          } else {
              AccountMemoryCache.put(jwtToken.getAuthAccount(), latestAccount);
              return latestAccount;
          }
      }
    
      @Override
      public String applyToken(AuthCredential credential) {
          BaseJwtAuthToken authToken = this.initBaseJwtAuthToken(credential);
    
          try {
              Subject subject = SecurityUtils.getSubject();
              subject.login(authToken);
              if (subject.isAuthenticated()) {
                  log.debug("申请token成功!authtoken={}", authToken.getCredentials());
                  this.saveLoginTrace(authToken, true);
                  return (String) authToken.getCredentials();
              } else {
                  log.error("认证失败");
                  this.saveLoginTrace(authToken, false);
                  throw new BusinessException(Status.FAIL_OPERATION, "认证失败");
              }
          } catch (Exception var4) {
              log.error("登录异常", var4);
              this.saveLoginTrace(authToken, false);
              throw new BusinessException(Status.FAIL_OPERATION, var4.getMessage());
          }
      }
    
      /**
      * 初始化token
      * @param {Object} AuthCredential credential
      */
      private BaseJwtAuthToken initBaseJwtAuthToken(AuthCredential credential) {
          BaseJwtAuthToken token = new BaseJwtAuthToken(this.getAuthType(), credential.getUserTypeClass());
          token.setAuthAccount(credential.getAuthAccount());
          token.setRememberMe(credential.isRememberMe());
          return token.generateAuthtoken(this.getExpiresInMinutes());
      }
    
      /**
       * 保存日志
       * @param authToken
       * @param isSuccess
       */
      protected void saveLoginTrace(BaseJwtAuthToken authToken, boolean isSuccess) {
          IamLoginTrace loginTrace = new IamLoginTrace();
          loginTrace.setAuthType(this.getAuthType()).setAuthAccount(authToken.getAuthAccount()).setUserType(authToken.getUserType()).setSuccess(isSuccess);
          BaseLoginUser currentUser = (BaseLoginUser) IamSecurityUtils.getCurrentUser();
          if (currentUser != null) {
              loginTrace.setUserId(currentUser.getId());
          }
    
          String userAgent = this.request.getHeader("user-agent");
          String ipAddress = IamSecurityUtils.getRequestIp(this.request);
          loginTrace.setUserAgent(userAgent).setIpAddress(ipAddress);
    
          try {
              this.iamLoginTraceService.createEntity(loginTrace);
          } catch (Exception var8) {
              log.warn("保存登录日志异常", var8);
          }
    
      }
    }
    
  • 修改登陆接口

    /**
     * 用户登录获取token
     * 基于无状态,增加了一个长期的refreshtoken,根据自己需要添加
     *
     * @param credential
     * @return
     * @throws Exception
     */
    @PostMapping("/auth/login")
    public JsonResult login(@RequestBody WXMemberCredential credential) throws Exception {
      // 替换service 获取token
      String authtoken = AuthServiceFactory.getAuthService(Cons.DICTCODE_AUTH_TYPE.WX_MP.name()).applyToken(credential);
      return JsonResult.OK(new HashMap<String, String>(8){{
          put("authtoken", authtoken);
          // 刷新token,长期有效
          put("refreshtoken", JwtUtils.generateToken(credential.getAuthAccount(), 60 * 24 * 30 * 12 * 10));
      }});
    }

    至此,我们将自定义扩展登陆、用户信息处理完毕!!!

总结

diboot文档

diboot 简单高效的轻代码开发框架 (欢迎star)


uucoding
30 声望1 粉丝

做好最小的每一件事