Java安全——Spring Boot集成Apache Shiro环境

apache shiro是灵活可用的安全框架,本篇文章的Shiro基本配置代码已传至github,欢迎点击获取,您可以直接在此基础上进行项目的二次开发。

1. 环境准备

首先,先创建springboot项目(这里我的版本号采用的是spring boot2.0),选择引入web、mysql、mybatis的依赖
创建完成后再手动引入shiro、druid连接池、工具包、jsp等的依赖

Apache Shiro不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。我们需要设计一套用户权限的相关体系,这里基于RBAC模型设计出User类、Role类和Permission类(省略getter/setter)。

public class User {
    private Integer uid;
    private String username;;
    private String password;
    private Set<Role> roles = new HashSet<>();
}

public class Role {
    private Integer rid;
    private String rname;
    private Set<Permission> permissions = new HashSet<>();
    private Set<User> users = new HashSet<>();
}

public class Permission {
    private Integer pid;
    private String name;
    private String url;
}

然后创建好test数据库、user、role和permission及其关联表,并创建service层和dao层,因内容过多,这里不展示了,请直接查看源码

2. 配置Shiro

完成上面准备后,现在真正开始Shiro的环境搭建。首先,创建一个AuthRealm类并继承AuthorizingRealm。
这里介绍下:
Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
下面是AuthRealm类的实现代码:

public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;
    // principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。
    // 一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
        List<String> permissionList = new ArrayList<>();
        List<String> roleList = new ArrayList<>();

        Set<Role> roleSet = user.getRoles();
        if (!CollectionUtils.isEmpty(roleSet)) {
            roleSet.forEach(role -> {
                roleList.add(role.getRname());

                Set<Permission> permissionSet = role.getPermissions();
                if (!CollectionUtils.isEmpty(permissionSet)) {
                    permissionList.addAll(permissionSet.stream()
                                                    .map(permission -> permission.getName())
                                                    .collect(Collectors.toList()));
                }
            });
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(roleList);
        info.addStringPermissions(permissionList);
        return info;
    }

    /**
     * Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
     * 登录时调用subject.login(token),Subject会委托SecuriyManager执行,SecuriyManager调用它的Realm执行会跳转到这里,
     * 本例中 /loginUser 接口创建的UsernamePasswordToken会传到这里。
     *
     * 执行流程:
     * 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;
     * 如果user找到但锁定了抛出锁定异常LockedAccountException;
     * 最后生成AuthenticationInfo信息,交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,
     * 如果不匹配将抛出密码错误异常IncorrectCredentialsException;
     * 另外如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException;
     * 在组装SimpleAuthenticationInfo信息时,需要传入:身份信息(用户名)、凭据(密文密码)、盐(username+salt),
     * CredentialsMatcher使用盐加密传入的明文密码和此处的密文密码进行匹配。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        User user = userService.findByUsername(username);
        return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
    }
}

然后,我们创建一个CredentialMatcher类继承SimpleCredentialsMatcher,这是用于密码校验的。
代码实现如下:

public class CredentialMatcher extends SimpleCredentialsMatcher {
    // 匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密)
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String (usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();
        return this.equals(password, dbPassword);
    }
}

最后,我们创建ShiroConfiguration类,用来集成管理Shiro的所有配置。其中包括将上面创建的CredentialMatcher密码类设置到AuthRealm类中,将AuthRealm类设置到SecuriryManager类中,使用Shiro的内置过滤器,shiro和Spring关联,开启AOP代理等。
代码实现如下:

@Configuration
public class ShiroConfiguration {

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);

        // 登录接口
        bean.setLoginUrl("/login");
        // 验证成功接口
        bean.setSuccessUrl("/index");
        // 未验证接口
        bean.setUnauthorizedUrl("/unauthorized");

        // public enum DefaultFilter {...}
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 【authc】:是org.apache.shiro.web.filter.authc.FormAuthenticationFilter类型的实例,其用于实现基于表单的身份验证
        filterChainDefinitionMap.put("/index", "authc");

        // 【anon】:表示不需要登录即可访问
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");

        // 【"/admin", "roles[admin]"】:表示只有角色为admin的用户可以访问/admin接口
        filterChainDefinitionMap.put("/admin", "roles[admin]");

        // 【"/edit", "perms[edit]"】:表示拥有edit权限才能访问/edit接口
        filterChainDefinitionMap.put("/edit", "perms[edit]");

        // **:匹配路径中的零个或多个路径,如/admin/**将匹配/admin/a或/admin/a/b。
        // 【"/druid/**", "anon"】:表示不拦截访问/druid/下的任意请求
        filterChainDefinitionMap.put("/druid/**", "anon");

        // 【user】:认证过滤器,表示必须存在用户,当登入操作时不做检查
        filterChainDefinitionMap.put("/**", "user");

        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }

    // SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且管理着所有 Subject;
    // 它是 Shiro 的核心,负责与后边介绍的其他组件进行交互,类似于SpringMVC 中的 DispatcherServlet 前端控制器;
    @Bean("securityManager")
    public SecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);
        return manager;
    }

    // Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,
    // 那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限
    // 进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher credentialMatcher) {
        AuthRealm authRealm = new AuthRealm();
        authRealm.setCredentialsMatcher(credentialMatcher);
        return authRealm;
    }

    // 自定义的密码校验
    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher() {
        return new CredentialMatcher();
    }

    // 用于开启Shiro Spring AOP权限注解的支持
    // 处理shiro和spring关联,让spring管理shiro时使用我们自定义的SecurityManager
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(manager);
        return advisor;
    }

    // 处理shiro和spring关联,使用AOP代理类
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

3. 测试

现在我们创建一个Controller类,提供ShiroConfiguration配置类中注入过滤器配置的接口【相关的JSP请查看源码获取】。
代码实现如下:

@Controller
public class TestController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }
        return "login";
    }

    @RequestMapping("/unauthorized")
    public String unauthorized() {
        return "unauthorized";
    }

    @RequestMapping("/edit")
    @ResponseBody
    public String edit() {
        return "edit success";
    }

    @GetMapping("/admin")
    @ResponseBody
    public String admin() {
        return "admin success";
    }

    @PostMapping("/loginUser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session) {
        // 创建用户名/密码身份验证Token
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        // Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,
        // 如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互
        // 都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            User user = (User) subject.getPrincipal();
            session.setAttribute("user", user);
            return "index";
        } catch (Exception e) {
            return "login";
        }
    }
}

4. 最后

感谢您阅读我的文章,文笔不好,能力有限,以上表达不当的地方还望指出。

阅读更多
换一批

没有更多推荐了,返回首页