Shiro加盐验证/存储用户信息

3,100 阅读3分钟

前言

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相关的内容,加深认知;

文中如有疑问或不解,欢迎指正!