Shiro加鹽驗證/儲存使用者資訊

JasmineR發表於2019-04-15

前言

Spring Boot整合Shiro進行身份認證等必須的前置性工作各位可以網上搜尋一番,本篇主要是對使用者登入驗證自定義加密驗證以及Shiro儲存登入使用者便於後續使用的內容。

1.1 使用者郵箱登入

注:使用者在配置Shiro登入地址時,實際執行登入驗證邏輯請求URL要設定為不進行驗證(新手)

ShiroConfig.java

 /**
   * Shiro Filter
   *
   * @param securityManager securityManager
   * @return shiroFilterFactoryBean
   */
  @Bean
  public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    log.info("======== Shiro config ==========");

    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // jwt過濾器
    Map<String, Filter> filterMap = shiroFilterFactoryBean.getFilters();
    filterMap.put("jwt", new JWTFilter());
    shiroFilterFactoryBean.setFilters(filterMap);
    shiroFilterFactoryBean.setUnauthorizedUrl("/401");
    // 設定登入路徑
    shiroFilterFactoryBean.setLoginUrl("/login");
    // 攔截器
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    // 實際登入地址,不能為/login
    filterChainDefinitionMap.put("/doLogin", "anon");
    filterChainDefinitionMap.put("/login", "anon");
    ......
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
  }
複製程式碼

使用者郵箱登入:郵箱作為賬號;

LoginController.java

/**
 * login
 *
 * @param email 登入郵箱
 * @param password  登入密碼
 * @return login
 */
@PostMapping("/doLogin")
public String login(String email, String password) {
    // 建立Subject例項
    Subject subject = SecurityUtils.getSubject();
    // 封裝使用者資料
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(email, password);
    // 登入判斷
    try {
        subject.login(usernamePasswordToken);
        if (subject.isAuthenticated()) {
            return "redirect:/customers";
        }
    } catch (UnknownAccountException e) {
        log.info("---> {}登入失敗", email);
    }

    return "login";
    }
複製程式碼

1.2 使用者密碼加鹽驗證

注:Shiro本身支援MD5加密驗證,使用HashedCredentialsMatcher配置加密規則進行加密

/**
 * 這裡需要設定成與PasswordEncrypter類相同的加密規則
 *
 * 在doGetAuthenticationInfo認證登陸返回SimpleAuthenticationInfo時會使用hashedCredentialsMatcher
 * 把使用者填入密碼加密後生成雜湊碼與資料庫對應的雜湊碼進行對比
 *
 * HashedCredentialsMatcher會自動根據AuthenticationInfo的型別是否是SaltedAuthenticationInfo來獲取credentialsSalt鹽
 *
 * @return
 */
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 雜湊演算法, 與註冊時使用的雜湊演算法相同
    hashedCredentialsMatcher.setHashIterations(2);// 雜湊次數, 與註冊時使用的雜湊冊數相同
    hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);// 生成16進位制, 與註冊時的生成格式相同
    return hashedCredentialsMatcher;
}
複製程式碼

然而並沒有使用本身自帶的加密方式進行使用者加鹽加密儲存,所以需要在Realm中重寫setCredentialsMatcher方法,保證自己加密和驗證的統一(自定義),我選擇重寫方法,而非自定義加密驗證類。(如下驗證Realm類程式碼)

AuthRealmForWeb.java

/**
 * 校驗使用者身份
 *
 * @param auth
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
    String email = (String) auth.getPrincipal();

    Optional<User> userOptional = userService.findByEmail(email);
    if (!userOptional.isPresent()) {
        throw new UnknownAccountException("未查詢到該使用者資訊");
    }
    User user = userOptional.get();

    // 此處第一個引數傳遞user,則將登入user資訊儲存備用
    // 如果使用加鹽驗證,則第三個引數必須使用ByteSource.Util.bytes(xxx)
    SimpleAuthenticationInfo authenticationInfo =
            new SimpleAuthenticationInfo(user, user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), getName());

    return authenticationInfo;
}

/**
 * Authorizaton 授權
 *
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    User user = (User) principals.getPrimaryPrincipal();

    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

    // 賦予登入使用者許可權
    Optional<List<Role>> optionalRoleList = userRoleService.findRolesByUserId(user.getId());
    if (optionalRoleList.isPresent()) {
        // 授權
        for (Role role : optionalRoleList.get()) {
            authorizationInfo.addRole(role.getName());
            Optional<List<Permission>> optionalPermissions = rolePermissionService.findPermissionsByRoleIds(
                    Arrays.asList(role.getId()));
            if (optionalPermissions.isPresent()) {
                authorizationInfo.addStringPermissions(optionalPermissions.get()
                        .stream().map(Permission::getPval).collect(Collectors.toList()));
            }
        }
    }

    return authorizationInfo;
}

 @Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
    credentialsMatcher = (token, info) -> {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        // 驗證時傳遞的加密鹽
        String salt = new String(((SimpleAuthenticationInfo) info).getCredentialsSalt().getBytes());
        // 登入錄入的密碼
        String password = new String(usernamePasswordToken.getPassword());
        // 自定義的加鹽加密方式
        String realPassword = Hashing.sha512().hashString(password + salt,
                Charsets.UTF_8).toString().substring(0, 17);

        return realPassword.equalsIgnoreCase(info.getCredentials().toString());
    };
    super.setCredentialsMatcher(credentialsMatcher);
}
複製程式碼

1.3 儲存使用者資訊

儲存使用者資訊如1.2上節所示,只需將第一個引數設定為當前使用者即可:

// 此處第一個引數傳遞user,則將登入user資訊儲存備用
    // 如果使用加鹽驗證,則第三個引數必須使用ByteSource.Util.bytes(xxx)
    SimpleAuthenticationInfo authenticationInfo =
            new SimpleAuthenticationInfo(user, user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), getName());
複製程式碼

儲存使用者資訊後,如何取出儲存的使用者資訊?很簡單,就儲存在Subject中如下:

User currentUser = (User) SecurityUtils.getSubject().getPrincipal();
複製程式碼

總結

Shrio聽說過但我本身未使用,只是專案中使用突擊一下,網上一篇篇的發現並不適合自己需要,沒有相關的程式碼;此篇結合自己專案的實際需要,記錄一下,後續有機會更新Shiro相關的內容,加深認知;

文中如有疑問或不解,歡迎指正!

相關文章