Contents
  1. 1. 逻辑概括
    1. 1.1. 加密逻辑
      1. 1.1.1. 代码
    2. 1.2. 触发权限校验的逻辑
  2. 2. 代码实现
  3. 3. salt相关
  4. 4. 添加验证码

https://blog.csdn.net/qq_31080089/article/details/53715910

参考源码: (设置了密码器、包含用户、角色创建、权限分配、ehcache缓存等模块的Demo)
https://github.com/huangzhenshi/spring_shiro

逻辑概括

加密逻辑

  1. 不设置加密器,只要不配置credentialsMatcher的Bean即可。也可以直接在代码中进行一次MD5加密。

  2. 设置加密器,复杂的MD5值加密,且多次加密,且加salt处理(可以参考纯洁的微笑的 SB shiro的Demo),校验密码时会调用有加密器的方法 doCredentialsMatch()

有加密器数据流程: 用户账号/密码 –> 用户账号去数据库中查询 数据库中的信息,包括salt值 –> 用户登录密码 +密码通过shiro配置文件中加密方式+加密次数+数据库的info中的salt值 –> 从而生成登录加密后的password信息,比对info的password信息,从而实现密码验证

代码

  • 没有加密器,直接调用 getCredentials(token)

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenCredentials = getCredentials(token);
    Object accountCredentials = getCredentials(info);
    return equals(tokenCredentials, accountCredentials);
    }
  • 有设置加密器,调用 hashProvidedCredentials(token, info),返回加密处理后的密码值

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
    }
    protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
    String hashAlgorithmName = assertHashAlgorithmName();
    return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
    }

触发权限校验的逻辑

  1. 手动触发,不依赖shiro-web.jar,设置/login = anon,在login逻辑里面触发验证。设置index.jsp为 loginUrl,这样项目进入的时候跳转到 index.jsp的登录界面,输入密码,提交到/login后台进行验证

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" />
    <!-- 核心配置,进入用户登录的页面,而不需要做权限认证拦截 -->
    <property name="loginUrl" value="/index.jsp"/>
    <property name="unauthorizedUrl" value="/403.jsp" />
    <property name="filterChainDefinitions">
    <value>
    /resources/** = anon
    /login = anon
    /logout = logout
    /** =authc
    </value>
    </property>
    </bean>
  2. 配置触发,需要依赖shiro-web.jar,博客上说,第一次访问loginUrl的时候判断是不是表单提交,不是就跳转到对应的index.jsp里面,第二次表单提交的后台地址也是loginUrl,出触发权限验证,如果验证成功则跳转到 SuccessUrl界面。但是我试了很多blog的代码,不可行,实际验证成功未跳转到 successUrl

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.action"/>
    <property name="successUrl" value="/first.action"/>

代码实现

  1. ShiroFilter配置

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" />
    <!-- 核心配置,进入用户登录的页面,而不需要做权限认证拦截 -->
    <property name="loginUrl" value="/index.jsp"/>
    <property name="unauthorizedUrl" value="/403.jsp" />
    <!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
    <property name="filterChainDefinitions">
    <value>
    <!-- 核心配置,开放静态资源、开放登录提交、设置注销链接Shiro会注销掉session -->
    /resources/** = anon
    /login = anon
    /logout = logout
    /** =authc
    </value>
    </property>
    </bean>
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="customRealm" />
    </bean>
    <!-- 自定义 realm -->
    <bean id="customRealm" class="com.light.ac.web.realm.CustomRealm">
    <property name="credentialsMatcher" ref="credentialsMatcher"/>
    </bean>
    <!-- 凭证匹配器 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    <property name="hashAlgorithmName" value="md5"/>
    <property name="hashIterations" value="1"/>
    </bean>
    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
    <!-- 开启shiro注解支持 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager" />
    </bean>
  2. 登录认证的时候会调用AuthenticatingRealm.getAuthenticationInfo()方法:

  • 用登录token去从重写的realm并从数据库里面获取对应的info
  • 比对token和info的password加密后的值是否相同,也就是密码是否匹配
    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) {
    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    // 会调用自定义realm里面的认证代码
    if (info == null) {
    info = doGetAuthenticationInfo(token);
    }
    // 会check token里面的密码和数据库info里面的是否一致
    if (info != null) {
    assertCredentialsMatch(token, info);
    }
    }
  1. 重写登录验证Realm(不带salt值认证的)

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // 获取用户名
    String userName = (String) token.getPrincipal();
    // 通过用户名获取用户对象
    User user = this.userService.findUserByUserName(userName);
    if (user == null) {
    return null;
    }
    // 通过 userId 获取该用户拥有的所有权限,返回值根据自己要求设置,并非固定值。
    Map<String,List<Permission>> permissionMap = this.permissionService.getPermissionMapByUserId(user.getId());
    user.setMenuList(permissionMap.get("menuList"));
    user.setPermissionList(permissionMap.get("permissionList"));
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
    return info;
    }
  2. 比对用户输出的密码和数据库中的密码(需要从shiro配置中获取到 加密的方式 MD5,加密的次数 N,另外从数据库的info中获取到salt值。
    如果生成Info的构造函数里面有salt参数的话,则hashProvidedCredentials的实例中会加入salt值参数,如果构造函数中没有salt,则实例不会进行salt操作。

  • 相关源码 HashedCredentialsMatcher.doCredentialsMatch
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info){
    CredentialsMatcher cm = getCredentialsMatcher();
    if (cm != null) {
    if (!cm.doCredentialsMatch(token, info)) {}
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
    }
    protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
    Object salt = null;
    if (info instanceof SaltedAuthenticationInfo) {
    salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
    } else {
    if (isHashSalted()) {
    salt = getSalt(token);
    }
    }
    return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
    }
  1. login方法主动触发用户验证和反馈

    @RequestMapping("login")
    @ResponseBody
    public Result login(String userName,String password) {
    UsernamePasswordToken token = new UsernamePasswordToken(userName.trim(), password);
    Subject subject = SecurityUtils.getSubject();
    try {
    subject.login(token);
    }catch (UnknownAccountException e) {
    return Result.fail(403, "用户名不存在");
    }catch(IncorrectCredentialsException e) {
    return Result.fail(403, "密码不正确");
    }
    return Result.succeed("/manageUI");
    }
  2. 创建用户或者修改用户密码时,生成加密密码的代码

    public void test(){
    int hashIterations = 2;//加密的次数
    Object salt = "8d78869f470951332959580424d4bf4f";//盐值
    String pwd="123456";
    Object credentials = pwd.toCharArray();
    String hashAlgorithmName = "md5";//加密方式
    //密码值
    Object simpleHash = new SimpleHash(hashAlgorithmName, credentials,
    ByteSource.Util.bytes(username+salt), hashIterations);
    System.out.println("加密前的值"+pwd+". 加密后的密码值:----->" + simpleHash);
    }

salt相关

  1. salt的好处
  • 仅仅知道加密方式(MD5)和加密次数(1次)也无法破解数据库中的密码,因为密码生成是夹杂了salt值处理
  • 不同用户,相同的密码,如果salt值不同,则数据库存储的密码也不同
  • salt值的选取,可以取一串无意义的数字+Username,或者一串无意义的数字+UserId,或者干脆数据库中不存储salt值,加密的时候直接取用户ID
    // 纯洁的微笑的demo salt值取的是: 用户名+salt
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    userInfo, //用户名
    userInfo.getPassword(), //密码
    ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
    getName() //realm name
    );
    public String getCredentialsSalt(){
    return this.username+this.salt;
    }
  1. 如果不使用salt值的配置,重写Realm里,返回info的构造函数不要包含 salt值,如下
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());

添加验证码

  1. 在webapp下 创建validatecode.jsp

    核心代码:
    //设置页面不缓存
    response.setHeader("Pragma", "No-cache");
    response.setHeader("Cache-Control", "no-cache");
    response.setDateHeader("Expires", 0);
  2. 嵌入在index.jsp,并添加刷新的方法

    <input id="randomcode" name="randomcode" size="8" />
    <img id="randomcode_img" src="validatecode.jsp" alt="" width="56" height="20" align='absMiddle' />
    <a href=javascript:randomcode_refresh()>刷新</a>
    # 刷新方法参数要 有时间戳才会触发刷新
    function randomcode_refresh(){
    var checkNumImage_=document.getElementById("randomcode_img");
    checkNumImage_.src="validatecode.jsp?timeStamp="+new Date().getTime();
    }
  3. 修改shiro,不过滤validatecode.jsp

    /validatecode.jsp=anon
  4. 修改login的逻辑,添加验证码验证

    @RequestMapping("login")
    @ResponseBody
    public Result login(String userName,String password,String randomcode,HttpServletRequest request) {
    HttpSession session=request.getSession();
    //取出session中的正确验证码
    String validateCode= (String) session.getAttribute("validateCode");
    if (randomcode!=null&&validateCode!=null&&!randomcode.equals(validateCode))
    {
    return Result.fail(403, "验证码输入错误");
    }
    }
Contents
  1. 1. 逻辑概括
    1. 1.1. 加密逻辑
      1. 1.1.1. 代码
    2. 1.2. 触发权限校验的逻辑
  2. 2. 代码实现
  3. 3. salt相关
  4. 4. 添加验证码