当前位置: 首页 > 工具软件 > Web-Return > 使用案例 >

springboot2 集成shiro-spring-boot-web-starter

锺离旻
2023-12-01


shiro是web开发中常用的使用安全管理框架,通过shiro-spring-boot-web-starter方式集成Shiro到springboot2可以简化配置。

1.引包

maven方式在项目pom.xml中引入shiro starter包的坐标,这里引用了1.4.1版本

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.1</version>
        </dependency>

2. 配置shiro

starter已经做了很多自动配置工作,具体可以参考ShiroAutoConfiguration.java、ShiroBeanAutoConfiguration.java和ShiroWebAutoConfiguration.java这几个文件。

2.1 配置类

这里使用新建shiroConfig.java类方式进行shiro配置。主要配置Realm、url过滤器、密码匹配器和安全管理器这几个组件就可以让shiro正常工作。

@Configuration
public class shiroConfig {

	// 配置自定义Realm
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(credentialsMatcher()); //配置使用哈希密码匹配
        return userRealm;
    }

	// 配置url过滤器
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/captcha", "anon");
        chainDefinition.addPathDefinition("/logout","anon");
        chainDefinition.addPathDefinition("/layuiadmin/**", "anon");
        chainDefinition.addPathDefinition("/druid/**", "anon");
        chainDefinition.addPathDefinition("/api/**", "anon");
        // all other paths require a logged in user
        chainDefinition.addPathDefinition("/login","anon");
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }

    // 设置用于匹配密码的CredentialsMatcher
    @Bean
    public HashedCredentialsMatcher credentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);  // 散列算法,这里使用更安全的sha256算法
        credentialsMatcher.setStoredCredentialsHexEncoded(false);  // 数据库存储的密码字段使用HEX还是BASE64方式加密
        credentialsMatcher.setHashIterations(1024);  // 散列迭代次数
        return credentialsMatcher;
    }

	// 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
    @Bean
    public SessionsSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }
   }

2.2 登录页配置

shiro默认的登录页是/login.jsp,需要在项目配置文件application.yml中修改默认登录页等配置。

shiro:
  loginUrl: /login
  successUrl: /

2.3 shiro默认过滤器

shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置过滤指定url的访问权限。

配置缩写对应的过滤器功能
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或“记住我”的用户才能访问

shiro常用的权限控制注解,可以在控制器类上使用

注解功能
@RequiresGuest只有游客可以访问
@RequiresAuthentication需要登录才能访问
@RequiresUser已登录的用户或“记住我”的用户能访问
@RequiresRoles已登录的用户需具有指定的角色才能访问
@RequiresPermissions已登录的用户需具有指定的权限才能访问

2.4 自定义Reaml类

Realm是实现自定义登录和授权的核心类,这里继承了抽象类AuthorizingRealm并重写doGetAuthenticationInfo(用于登录验证)和doGetAuthorizationInfo(用于权限验证)这俩个方法。doGetAuthenticationInfo主要作用是获取用户输入的用户名、密码等信息并从数据库中取出保存的密码交给shiro,由shiro的密码匹配器进行匹配。

    /**
     * 认证 ,用户名密码校验
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        User user = userDao.getByUsername(username);

        if (user == null) {
            throw new UnknownAccountException(); // 账号不存在
        }
        if (user.getStatus() != 0) {
            throw new LockedAccountException();  // 账号被锁定
        }
        String salt = user.getSalt();
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,
                user.getPassword(),
                ByteSource.Util.bytes(salt),
                getName());

        return authenticationInfo;
    }

根据用户输入的用户名,在数据库中查找到用户记录,并用查到的用户对象、数据库中存储的密码、密码盐和Realm对象名字构建一个认证信息对象(SimpleAuthenticationInfo)交给系统进行密码验证。
doGetAuthorizationInfo主要是获取用户的角色和权限,并交给Shiro去判断是否具有访问资源的权限。

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
      // 角色
        Set<String> roles = new HashSet<>();
      // 权限
      // 测试用权限
        if ("admin".equals(user.getUsername())) {
            roles.add("admin");
            permissions.add("op:write");
        } else {
            roles.add("user");
            permissions.add("op:read");
        }
        authorizationInfo.setRoles(roles);
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }

支持shiro的配置工作已经基本完成,已经可以正常工作了。接下来就是与前端配合完成页面的登录和权限控制等工作。

3 用户登录

3.1 页面设计

在需要登录的前端 html页面上,向后台登录url提交包含用户名和密码字段的表单。

3.2 登录处理

在后台登录url中,接收用户名密码,据此创建一个usernamePasswordToken令牌,交由Shiro并调用login()方法进行登录,如果不抛出任何异常表明登录成功,如果抛出异常,这根据异常种类返回提示出错信息给用户。

    @ResponseBody
    @PostMapping("login")
    public JsonResult doLogin(String username, String password, String vercode, HttpServletRequest request) {

        if ("".equals(username.trim()) || "".equals(password.trim())) {
            return JsonResult.error("账号或密码不能为空");
        }
        if (!CaptchaUtil.ver(vercode, request)) {
            //CaptchaUtil.clear(request);  // 清除session中的验证码
            return JsonResult.error("验证码不正确");
        }

        try {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            SecurityUtils.getSubject().login(token);
            //addLoginRecord(getLoginUserId(), request);  // 记录登录信息
            HashMap<String, String> map = new HashMap<>();
            map.put("access_token", "1111111111111111111");  // 模拟登录令牌
            return JsonResult.ok("登录成功").put("data", map);
        } catch (IncorrectCredentialsException ice) {
            return JsonResult.error("密码错误");
        } catch (UnknownAccountException uae) {
            return JsonResult.error("账号不存在");
        } catch (LockedAccountException e) {
            return JsonResult.error("账号被锁定");
        } catch (ExcessiveAttemptsException eae) {
            return JsonResult.error("操作频繁,请稍后再试");
        }
    }

验证码使用的是 EasyCaptcha工具

3.3 密码匹配原理

  1. 在登录后台url中,我们拿到用户输入的用户名和密码,并组成一个token传给了shiro的login()方法。
  2. login()方法进行登录验证过程中,调用Realm对象的doGetAuthenticationInfo()方法,在这里根据token中的用户名在数据库中查找对应用户,用用户对象、数据库中存储的密码、密码盐和Realm对象名字构建一个认证信息对象(SimpleAuthenticationInfo)交给系统进行密码验证。
  3. 根据在配置文件中配置的密码匹配器,调用doCredentialsMatch()方法进行密码匹配。默认是SimpleCredentialsMatcher匹配器,他是以明文方式进行用户输入的密码和数据库中保存的密码进行匹配。
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }

我们在配置中使用的是HashedCredentialsMatcher匹配器,使用更安全的sha256哈希算法,指定了数据库中密码字段使用base64方式加密。
那么在创建用户或修改密码时怎么生成加密密码呢?利用shiro提供的simplehash()方法就可以,如下指定了sha256算法,密码字符串,密码盐和迭代次数(需要和config配置里的次数相同),最后对生成的哈希密码串进行base64编码。

new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, ByteSource.Util.bytes(salt), hashIterations).toBase64();

4. 权限管理

4.1 url和注解组合使用

shiro可以使用url配置控制权限,也可以在控制器类上使用注解控制权限。同时使用两种配置方式灵活结合,才是适应不同应用场景的最佳实践。只用注解或只用url配置,都不够灵活,有时会很麻烦。思路是:

用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制

4.2 @RequireXXX注解可能的Bug

注意:解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。 加入这项配置能解决这个bug
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        //defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

4.3 注解权限

控制器上通过注解配置详细的角色和权限,多个权限和角色之间默认是“与”关系,可以通过logical参数设置为“或”。

    @RequiresRoles("user")
    @GetMapping("/user/list.html")
    public String userList() {
        return "user/user/list";
    }

    @RequiresPermissions("op:read")
    @GetMapping("user/userform.html")
    public String userForm() {
        return "user/user/userform";
    }

    @RequiresRoles("admin")
    @GetMapping("/administrators/list.html")
    public String adminList() {
        return "user/administrators/list";
    }

    @RequiresPermissions("op:write")
    @GetMapping("administrators/adminform.html")
    public String administratorForm() {
        return "user/administrators/adminform";
    }

    @RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
    @GetMapping("/administrators/role.html")
    public String roleList() {
        return "user/administrators/role";
    }
 类似资料: